Prettier format pending files
This commit is contained in:
parent
12127ff4ef
commit
5a7b56a792
9 changed files with 229 additions and 112 deletions
|
|
@ -60,7 +60,10 @@ const App = () => (
|
||||||
element={<Navigate to="/feed" replace />}
|
element={<Navigate to="/feed" replace />}
|
||||||
/>
|
/>
|
||||||
<Route path="/projects/new" element={<ProjectsNew />} />
|
<Route path="/projects/new" element={<ProjectsNew />} />
|
||||||
<Route path="/profile" element={<Navigate to="/profiles/me" replace />} />
|
<Route
|
||||||
|
path="/profile"
|
||||||
|
element={<Navigate to="/profiles/me" replace />}
|
||||||
|
/>
|
||||||
<Route path="/profiles" element={<ProfilesDirectory />} />
|
<Route path="/profiles" element={<ProfilesDirectory />} />
|
||||||
<Route path="/profiles/me" element={<ProfilePassport />} />
|
<Route path="/profiles/me" element={<ProfilePassport />} />
|
||||||
<Route path="/profiles/:id" element={<ProfilePassport />} />
|
<Route path="/profiles/:id" element={<ProfilePassport />} />
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,7 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import { CheckCircle, ArrowRight, Sparkles, ShieldCheck } from "lucide-react";
|
||||||
CheckCircle,
|
|
||||||
ArrowRight,
|
|
||||||
Sparkles,
|
|
||||||
ShieldCheck,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { AethexAchievement } from "@/lib/aethex-database-adapter";
|
import type { AethexAchievement } from "@/lib/aethex-database-adapter";
|
||||||
|
|
||||||
|
|
@ -211,16 +206,23 @@ export default function Welcome({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm text-emerald-100/90">
|
<CardContent className="space-y-3 text-sm text-emerald-100/90">
|
||||||
{achievement.description && <p>{achievement.description}</p>}
|
{achievement.description && <p>{achievement.description}</p>}
|
||||||
{typeof achievement.xp_reward === "number" && achievement.xp_reward > 0 && (
|
{typeof achievement.xp_reward === "number" &&
|
||||||
<div className="text-xs uppercase tracking-wider text-emerald-200">
|
achievement.xp_reward > 0 && (
|
||||||
+{achievement.xp_reward} XP added to your passport progression
|
<div className="text-xs uppercase tracking-wider text-emerald-200">
|
||||||
</div>
|
+{achievement.xp_reward} XP added to your passport progression
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Badge variant="outline" className="border-emerald-500/40 text-emerald-100">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-emerald-500/40 text-emerald-100"
|
||||||
|
>
|
||||||
<ShieldCheck className="mr-1 h-3.5 w-3.5" /> Profile Verified
|
<ShieldCheck className="mr-1 h-3.5 w-3.5" /> Profile Verified
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-emerald-500/40 text-emerald-100">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-emerald-500/40 text-emerald-100"
|
||||||
|
>
|
||||||
<Sparkles className="mr-1 h-3.5 w-3.5" /> Passport Updated
|
<Sparkles className="mr-1 h-3.5 w-3.5" /> Passport Updated
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -271,7 +273,10 @@ export default function Welcome({
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-aethex-500/20 text-aethex-100 border border-aethex-500/40"
|
className="bg-aethex-500/20 text-aethex-100 border border-aethex-500/40"
|
||||||
>
|
>
|
||||||
<Link to="/dashboard?tab=connections" className="flex items-center gap-2">
|
<Link
|
||||||
|
to="/dashboard?tab=connections"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
<ShieldCheck className="h-4 w-4" />
|
<ShieldCheck className="h-4 w-4" />
|
||||||
Link OAuth Accounts
|
Link OAuth Accounts
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -109,18 +109,29 @@ const PassportSummary = ({
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-white shadow-lg",
|
"text-white shadow-lg",
|
||||||
realm?.gradient ? `bg-gradient-to-r ${realm.gradient}` : "bg-aethex-500",
|
realm?.gradient
|
||||||
|
? `bg-gradient-to-r ${realm.gradient}`
|
||||||
|
: "bg-aethex-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{realm?.label || "Innovation Commons"}
|
{realm?.label || "Innovation Commons"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-slate-600/60 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-600/60 text-slate-200"
|
||||||
|
>
|
||||||
Level {level}
|
Level {level}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-slate-600/60 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-600/60 text-slate-200"
|
||||||
|
>
|
||||||
{totalXp} XP
|
{totalXp} XP
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-slate-600/60 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-600/60 text-slate-200"
|
||||||
|
>
|
||||||
{loyaltyPoints} Loyalty
|
{loyaltyPoints} Loyalty
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -150,7 +161,8 @@ const PassportSummary = ({
|
||||||
<Sparkles className="h-4 w-4 text-aethex-300" />
|
<Sparkles className="h-4 w-4 text-aethex-300" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm text-slate-200">
|
<p className="mt-2 text-sm text-slate-200">
|
||||||
{(profile as any)?.user_type?.replace("_", " ") || "community member"}
|
{(profile as any)?.user_type?.replace("_", " ") ||
|
||||||
|
"community member"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/40 p-4">
|
<div className="rounded-xl border border-slate-800 bg-slate-900/40 p-4">
|
||||||
|
|
@ -185,9 +197,7 @@ const PassportSummary = ({
|
||||||
className="rounded-xl border border-slate-800 bg-slate-900/40 p-4"
|
className="rounded-xl border border-slate-800 bg-slate-900/40 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-slate-100">
|
<div className="flex items-center gap-2 text-slate-100">
|
||||||
<span className="text-xl">
|
<span className="text-xl">{achievement.icon || "✨"}</span>
|
||||||
{achievement.icon || "✨"}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{achievement.name}
|
{achievement.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -210,7 +220,11 @@ const PassportSummary = ({
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{interests.map((interest) => (
|
{interests.map((interest) => (
|
||||||
<Badge key={interest} variant="outline" className="border-slate-600/60 text-slate-200">
|
<Badge
|
||||||
|
key={interest}
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-600/60 text-slate-200"
|
||||||
|
>
|
||||||
{interest}
|
{interest}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
@ -249,14 +263,20 @@ const PassportSummary = ({
|
||||||
<>
|
<>
|
||||||
<Separator className="border-slate-800" />
|
<Separator className="border-slate-800" />
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Button asChild variant="outline" className="border-slate-700 text-slate-100">
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700 text-slate-100"
|
||||||
|
>
|
||||||
<Link to="/dashboard?tab=profile">Edit Profile</Link>
|
<Link to="/dashboard?tab=profile">Edit Profile</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90"
|
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90"
|
||||||
>
|
>
|
||||||
<Link to="/dashboard?tab=connections">Manage Passport Links</Link>
|
<Link to="/dashboard?tab=connections">
|
||||||
|
Manage Passport Links
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export const aethexUserService = {
|
||||||
|
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ export const aethexUserService = {
|
||||||
|
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,18 +194,19 @@ export const aethexUserService = {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((data as any[]) || []).map((row) =>
|
return ((data as any[]) || []).map(
|
||||||
({
|
(row) =>
|
||||||
...(row as AethexUserProfile),
|
({
|
||||||
user_type: (row as any).user_type || "community_member",
|
...(row as AethexUserProfile),
|
||||||
experience_level: (row as any).experience_level || "beginner",
|
user_type: (row as any).user_type || "community_member",
|
||||||
}) as AethexUserProfile,
|
experience_level: (row as any).experience_level || "beginner",
|
||||||
|
}) as AethexUserProfile,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -238,7 +239,7 @@ export const aethexUserService = {
|
||||||
|
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +281,7 @@ export const aethexUserService = {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -315,7 +316,7 @@ export const aethexUserService = {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_interests\" is missing. Please run the required migrations.",
|
'Supabase table "user_interests" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -333,7 +334,7 @@ export const aethexUserService = {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_interests\" is missing. Please run the required migrations.",
|
'Supabase table "user_interests" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.warn("Error fetching interests:", error);
|
console.warn("Error fetching interests:", error);
|
||||||
|
|
@ -610,7 +611,7 @@ export const aethexAchievementService = {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Supabase table \"user_profiles\" is missing. Please run the required migrations.",
|
'Supabase table "user_profiles" is missing. Please run the required migrations.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.warn("Unable to load profile for XP update:", error);
|
console.warn("Unable to load profile for XP update:", error);
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,8 @@ class MockAuthService {
|
||||||
// Create new profile
|
// Create new profile
|
||||||
profile = {
|
profile = {
|
||||||
id: userId,
|
id: userId,
|
||||||
username: updates.username || this.currentUser?.email?.split("@")[0] || "user",
|
username:
|
||||||
|
updates.username || this.currentUser?.email?.split("@")[0] || "user",
|
||||||
email: this.currentUser?.email || "",
|
email: this.currentUser?.email || "",
|
||||||
role: "member",
|
role: "member",
|
||||||
onboarded: true,
|
onboarded: true,
|
||||||
|
|
@ -351,8 +352,9 @@ class MockAuthService {
|
||||||
return { data: { provider: removedProvider }, error: null };
|
return { data: { provider: removedProvider }, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
private authCallback: ((event: string, session: MockSession | null) => void) |
|
private authCallback:
|
||||||
null = null;
|
| ((event: string, session: MockSession | null) => void)
|
||||||
|
| null = null;
|
||||||
|
|
||||||
private notifyAuthChange(event: string) {
|
private notifyAuthChange(event: string) {
|
||||||
if (this.authCallback) {
|
if (this.authCallback) {
|
||||||
|
|
|
||||||
|
|
@ -135,16 +135,24 @@ describe("onboarding passport flow", () => {
|
||||||
bio: "Building awesome experiences",
|
bio: "Building awesome experiences",
|
||||||
});
|
});
|
||||||
|
|
||||||
const hydratedProfile = (await mockAuth.getUserProfile(user.id as any)) as AethexUserProfile;
|
const hydratedProfile = (await mockAuth.getUserProfile(
|
||||||
|
user.id as any,
|
||||||
|
)) as AethexUserProfile;
|
||||||
expect(checkProfileComplete(hydratedProfile as any)).toBe(true);
|
expect(checkProfileComplete(hydratedProfile as any)).toBe(true);
|
||||||
|
|
||||||
await mockAuth.linkIdentity({ provider: "github" });
|
await mockAuth.linkIdentity({ provider: "github" });
|
||||||
const refreshedUser = (await mockAuth.getUser()).data.user;
|
const refreshedUser = (await mockAuth.getUser()).data.user;
|
||||||
expect(refreshedUser?.identities?.some((id: any) => id.provider === "github")).toBe(true);
|
expect(
|
||||||
|
refreshedUser?.identities?.some((id: any) => id.provider === "github"),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
await aethexAchievementService.checkAndAwardOnboardingAchievement(user.id);
|
await aethexAchievementService.checkAndAwardOnboardingAchievement(user.id);
|
||||||
const achievements = await aethexAchievementService.getUserAchievements(user.id);
|
const achievements = await aethexAchievementService.getUserAchievements(
|
||||||
const welcomeBadge = achievements.find((item) => item.name === "Welcome to AeThex");
|
user.id,
|
||||||
|
);
|
||||||
|
const welcomeBadge = achievements.find(
|
||||||
|
(item) => item.name === "Welcome to AeThex",
|
||||||
|
);
|
||||||
expect(welcomeBadge).toBeTruthy();
|
expect(welcomeBadge).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@ export default function Onboarding() {
|
||||||
useState<AethexAchievement | null>(null);
|
useState<AethexAchievement | null>(null);
|
||||||
|
|
||||||
const mapProfileToOnboardingData = useCallback(
|
const mapProfileToOnboardingData = useCallback(
|
||||||
(profile: AethexUserProfile | null, interests: string[]): OnboardingData => {
|
(
|
||||||
|
profile: AethexUserProfile | null,
|
||||||
|
interests: string[],
|
||||||
|
): OnboardingData => {
|
||||||
const email = profile?.email || user?.email || "";
|
const email = profile?.email || user?.email || "";
|
||||||
const fullName = profile?.full_name?.trim() || "";
|
const fullName = profile?.full_name?.trim() || "";
|
||||||
const nameParts = fullName ? fullName.split(/\s+/).filter(Boolean) : [];
|
const nameParts = fullName ? fullName.split(/\s+/).filter(Boolean) : [];
|
||||||
|
|
@ -120,8 +123,7 @@ export default function Onboarding() {
|
||||||
userType: normalizedType,
|
userType: normalizedType,
|
||||||
personalInfo: {
|
personalInfo: {
|
||||||
firstName:
|
firstName:
|
||||||
firstName ||
|
firstName || (profile?.username ?? email.split("@")[0] ?? ""),
|
||||||
(profile?.username ?? email.split("@")[0] ?? ""),
|
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
company: (profile as any)?.company || "",
|
company: (profile as any)?.company || "",
|
||||||
|
|
@ -185,10 +187,7 @@ export default function Onboarding() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (typeof parsed.step === "number") {
|
if (typeof parsed.step === "number") {
|
||||||
nextStep = Math.max(
|
nextStep = Math.max(0, Math.min(parsed.step, steps.length - 1));
|
||||||
0,
|
|
||||||
Math.min(parsed.step, steps.length - 1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
restored = true;
|
restored = true;
|
||||||
}
|
}
|
||||||
|
|
@ -326,8 +325,7 @@ export default function Onboarding() {
|
||||||
username: normalizedFirst.replace(/\s+/g, "_"),
|
username: normalizedFirst.replace(/\s+/g, "_"),
|
||||||
full_name: `${normalizedFirst} ${normalizedLast}`.trim(),
|
full_name: `${normalizedFirst} ${normalizedLast}`.trim(),
|
||||||
user_type:
|
user_type:
|
||||||
(userTypeMap[data.userType || "member"] as any) ||
|
(userTypeMap[data.userType || "member"] as any) || "community_member",
|
||||||
"community_member",
|
|
||||||
experience_level: (data.experience.level as any) || "beginner",
|
experience_level: (data.experience.level as any) || "beginner",
|
||||||
bio: data.experience.previousProjects?.trim() || undefined,
|
bio: data.experience.previousProjects?.trim() || undefined,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ const ProfilePassport = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, linkedProviders } = useAuth();
|
const { user, linkedProviders } = useAuth();
|
||||||
|
|
||||||
const [profile, setProfile] = useState<(AethexUserProfile & { email?: string | null }) | null>(null);
|
const [profile, setProfile] = useState<
|
||||||
|
(AethexUserProfile & { email?: string | null }) | null
|
||||||
|
>(null);
|
||||||
const [achievements, setAchievements] = useState<AethexAchievement[]>([]);
|
const [achievements, setAchievements] = useState<AethexAchievement[]>([]);
|
||||||
const [projects, setProjects] = useState<ProjectPreview[]>([]);
|
const [projects, setProjects] = useState<ProjectPreview[]>([]);
|
||||||
const [interests, setInterests] = useState<string[]>([]);
|
const [interests, setInterests] = useState<string[]>([]);
|
||||||
|
|
@ -74,14 +76,15 @@ const ProfilePassport = () => {
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [profileData, achievementList, interestList, projectList] = await Promise.all([
|
const [profileData, achievementList, interestList, projectList] =
|
||||||
params.id === "me" && profile && profile.id === targetUserId
|
await Promise.all([
|
||||||
? Promise.resolve(profile)
|
params.id === "me" && profile && profile.id === targetUserId
|
||||||
: aethexUserService.getProfileById(targetUserId),
|
? Promise.resolve(profile)
|
||||||
aethexAchievementService.getUserAchievements(targetUserId),
|
: aethexUserService.getProfileById(targetUserId),
|
||||||
aethexUserService.getUserInterests(targetUserId),
|
aethexAchievementService.getUserAchievements(targetUserId),
|
||||||
aethexProjectService.getUserProjects(targetUserId).catch(() => []),
|
aethexUserService.getUserInterests(targetUserId),
|
||||||
]);
|
aethexProjectService.getUserProjects(targetUserId).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!profileData) {
|
if (!profileData) {
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
|
|
@ -142,20 +145,29 @@ const ProfilePassport = () => {
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-white">Highlighted missions</h2>
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
Highlighted missions
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-slate-300">
|
<p className="text-sm text-slate-300">
|
||||||
A snapshot of what this creator has shipped inside AeThex.
|
A snapshot of what this creator has shipped inside AeThex.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<Button asChild variant="outline" className="border-slate-700/70 text-slate-100">
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-100"
|
||||||
|
>
|
||||||
<Link to="/projects/new">Launch new project</Link>
|
<Link to="/projects/new">Launch new project</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<Card key={project.id} className="border border-slate-800 bg-slate-900/70">
|
<Card
|
||||||
|
key={project.id}
|
||||||
|
className="border border-slate-800 bg-slate-900/70"
|
||||||
|
>
|
||||||
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<CardTitle className="text-lg text-white">
|
<CardTitle className="text-lg text-white">
|
||||||
|
|
@ -165,15 +177,23 @@ const ProfilePassport = () => {
|
||||||
{project.description || "AeThex project"}
|
{project.description || "AeThex project"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="border-slate-700/70 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-200"
|
||||||
|
>
|
||||||
{project.status?.replace("_", " ") ?? "active"}
|
{project.status?.replace("_", " ") ?? "active"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center justify-between text-xs text-slate-300">
|
<CardContent className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-3.5 w-3.5" /> {formatDate(project.created_at)}
|
<Clock className="h-3.5 w-3.5" />{" "}
|
||||||
|
{formatDate(project.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-aethex-200">
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 text-xs text-aethex-200"
|
||||||
|
>
|
||||||
<Link to="/projects/new">
|
<Link to="/projects/new">
|
||||||
View mission
|
View mission
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
|
|
@ -189,26 +209,36 @@ const ProfilePassport = () => {
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-white">Achievements</h2>
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
Achievements
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-slate-300">
|
<p className="text-sm text-slate-300">
|
||||||
Passport stamps earned across AeThex experiences.
|
Passport stamps earned across AeThex experiences.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<Badge variant="outline" className="border-aethex-500/50 text-aethex-200">
|
<Badge
|
||||||
<Award className="mr-1 h-3 w-3" /> {achievements.length} badges
|
variant="outline"
|
||||||
|
className="border-aethex-500/50 text-aethex-200"
|
||||||
|
>
|
||||||
|
<Award className="mr-1 h-3 w-3" /> {achievements.length}{" "}
|
||||||
|
badges
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{achievements.length === 0 ? (
|
{achievements.length === 0 ? (
|
||||||
<Card className="border border-slate-800 bg-slate-900/60 p-8 text-center text-slate-300">
|
<Card className="border border-slate-800 bg-slate-900/60 p-8 text-center text-slate-300">
|
||||||
<Target className="mx-auto mb-3 h-8 w-8 text-aethex-300" />
|
<Target className="mx-auto mb-3 h-8 w-8 text-aethex-300" />
|
||||||
No achievements yet. Complete onboarding and participate in missions to earn AeThex badges.
|
No achievements yet. Complete onboarding and participate in
|
||||||
|
missions to earn AeThex badges.
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{achievements.map((achievement) => (
|
{achievements.map((achievement) => (
|
||||||
<Card key={achievement.id} className="border border-slate-800 bg-slate-900/70">
|
<Card
|
||||||
|
key={achievement.id}
|
||||||
|
className="border border-slate-800 bg-slate-900/70"
|
||||||
|
>
|
||||||
<CardContent className="flex h-full flex-col justify-between gap-3 p-5">
|
<CardContent className="flex h-full flex-col justify-between gap-3 p-5">
|
||||||
<div className="flex items-center gap-3 text-white">
|
<div className="flex items-center gap-3 text-white">
|
||||||
<span className="text-3xl">
|
<span className="text-3xl">
|
||||||
|
|
@ -241,24 +271,40 @@ const ProfilePassport = () => {
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-white">Stay connected</h2>
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
Stay connected
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-slate-300">
|
<p className="text-sm text-slate-300">
|
||||||
Reach out, collaborate, and shape the next AeThex release together.
|
Reach out, collaborate, and shape the next AeThex release
|
||||||
|
together.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isSelf ? (
|
{isSelf ? (
|
||||||
<Button asChild className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90">
|
<Button
|
||||||
<Link to="/dashboard?tab=connections">Manage connections</Link>
|
asChild
|
||||||
|
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90"
|
||||||
|
>
|
||||||
|
<Link to="/dashboard?tab=connections">
|
||||||
|
Manage connections
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant="outline" className="border-slate-700/70 text-slate-100">
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-100"
|
||||||
|
>
|
||||||
<Link to="/dashboard">Invite to collaborate</Link>
|
<Link to="/dashboard">Invite to collaborate</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 text-sm text-slate-300">
|
<div className="flex flex-wrap gap-2 text-sm text-slate-300">
|
||||||
{profile.github_url && (
|
{profile.github_url && (
|
||||||
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 text-xs text-slate-200"
|
||||||
|
>
|
||||||
<a href={profile.github_url} target="_blank" rel="noreferrer">
|
<a href={profile.github_url} target="_blank" rel="noreferrer">
|
||||||
GitHub
|
GitHub
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
|
|
@ -266,31 +312,58 @@ const ProfilePassport = () => {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{profile.linkedin_url && (
|
{profile.linkedin_url && (
|
||||||
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
|
<Button
|
||||||
<a href={profile.linkedin_url} target="_blank" rel="noreferrer">
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 text-xs text-slate-200"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={profile.linkedin_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
LinkedIn
|
LinkedIn
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{profile.twitter_url && (
|
{profile.twitter_url && (
|
||||||
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
|
<Button
|
||||||
<a href={profile.twitter_url} target="_blank" rel="noreferrer">
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 text-xs text-slate-200"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={profile.twitter_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
X / Twitter
|
X / Twitter
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{profile.website_url && (
|
{profile.website_url && (
|
||||||
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
|
<Button
|
||||||
<a href={profile.website_url} target="_blank" rel="noreferrer">
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 text-xs text-slate-200"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={profile.website_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
Portfolio
|
Portfolio
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{profile.bio && (
|
{profile.bio && (
|
||||||
<Badge variant="outline" className="border-slate-700/70 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-200"
|
||||||
|
>
|
||||||
{profile.bio}
|
{profile.bio}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,7 @@ import {
|
||||||
type AethexUserProfile,
|
type AethexUserProfile,
|
||||||
} from "@/lib/aethex-database-adapter";
|
} from "@/lib/aethex-database-adapter";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { Search, RefreshCw, UserRound, Users, Sparkles } from "lucide-react";
|
||||||
Search,
|
|
||||||
RefreshCw,
|
|
||||||
UserRound,
|
|
||||||
Users,
|
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
const realmFilters: Array<{ value: string; label: string }> = [
|
const realmFilters: Array<{ value: string; label: string }> = [
|
||||||
{ value: "all", label: "All Realms" },
|
{ value: "all", label: "All Realms" },
|
||||||
|
|
@ -52,7 +46,8 @@ interface ProfileCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileCard = ({ profile }: ProfileCardProps) => {
|
const ProfileCard = ({ profile }: ProfileCardProps) => {
|
||||||
const realmStyle = realmBadgeStyles[profile.user_type] || "bg-aethex-500 text-white";
|
const realmStyle =
|
||||||
|
realmBadgeStyles[profile.user_type] || "bg-aethex-500 text-white";
|
||||||
return (
|
return (
|
||||||
<Card className="group h-full border border-slate-800 bg-slate-900/60 transition-transform hover:-translate-y-1 hover:border-aethex-400/60">
|
<Card className="group h-full border border-slate-800 bg-slate-900/60 transition-transform hover:-translate-y-1 hover:border-aethex-400/60">
|
||||||
<CardHeader className="space-y-3">
|
<CardHeader className="space-y-3">
|
||||||
|
|
@ -60,7 +55,10 @@ const ProfileCard = ({ profile }: ProfileCardProps) => {
|
||||||
<Badge className={cn("text-xs uppercase tracking-wider", realmStyle)}>
|
<Badge className={cn("text-xs uppercase tracking-wider", realmStyle)}>
|
||||||
{profile.user_type.replace("_", " ")}
|
{profile.user_type.replace("_", " ")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-slate-700/70 text-slate-300">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-300"
|
||||||
|
>
|
||||||
Level {profile.level ?? 1}
|
Level {profile.level ?? 1}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,12 +90,19 @@ const ProfileCard = ({ profile }: ProfileCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 text-xs text-slate-300">
|
<div className="flex flex-wrap gap-2 text-xs text-slate-300">
|
||||||
{(profile as any)?.skills?.slice(0, 3)?.map((skill: string) => (
|
{(profile as any)?.skills?.slice(0, 3)?.map((skill: string) => (
|
||||||
<Badge key={skill} variant="outline" className="border-slate-700/70 text-slate-200">
|
<Badge
|
||||||
|
key={skill}
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-200"
|
||||||
|
>
|
||||||
{skill}
|
{skill}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{((profile as any)?.skills?.length || 0) > 3 && (
|
{((profile as any)?.skills?.length || 0) > 3 && (
|
||||||
<Badge variant="outline" className="border-slate-700/70 text-slate-200">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-700/70 text-slate-200"
|
||||||
|
>
|
||||||
+{((profile as any)?.skills?.length || 0) - 3}
|
+{((profile as any)?.skills?.length || 0) - 3}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -107,7 +112,10 @@ const ProfileCard = ({ profile }: ProfileCardProps) => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700/70 text-slate-100 transition-colors hover:border-aethex-400/60 hover:text-white"
|
className="w-full border-slate-700/70 text-slate-100 transition-colors hover:border-aethex-400/60 hover:text-white"
|
||||||
>
|
>
|
||||||
<Link to={`/profiles/${profile.id}`} className="flex items-center justify-center gap-2">
|
<Link
|
||||||
|
to={`/profiles/${profile.id}`}
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
<UserRound className="h-4 w-4" />
|
<UserRound className="h-4 w-4" />
|
||||||
View Passport
|
View Passport
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -129,15 +137,9 @@ const ProfilesDirectory = () => {
|
||||||
const matchesRealm =
|
const matchesRealm =
|
||||||
realmFilter === "all" || profile.user_type === realmFilter;
|
realmFilter === "all" || profile.user_type === realmFilter;
|
||||||
const matchesSearch = lowerSearch
|
const matchesSearch = lowerSearch
|
||||||
? [
|
? [profile.full_name, profile.username, (profile as any)?.company]
|
||||||
profile.full_name,
|
|
||||||
profile.username,
|
|
||||||
(profile as any)?.company,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) =>
|
.some((value) => String(value).toLowerCase().includes(lowerSearch))
|
||||||
String(value).toLowerCase().includes(lowerSearch),
|
|
||||||
)
|
|
||||||
: true;
|
: true;
|
||||||
return matchesRealm && matchesSearch;
|
return matchesRealm && matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
@ -178,7 +180,8 @@ const ProfilesDirectory = () => {
|
||||||
Discover AeThex talent
|
Discover AeThex talent
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-300">
|
<p className="text-slate-300">
|
||||||
Browse verified creators, clients, and community members across every AeThex realm.
|
Browse verified creators, clients, and community members
|
||||||
|
across every AeThex realm.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
@ -190,7 +193,10 @@ const ProfilesDirectory = () => {
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90">
|
<Button
|
||||||
|
asChild
|
||||||
|
className="bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90"
|
||||||
|
>
|
||||||
<Link to="/profiles/me">Go to my passport</Link>
|
<Link to="/profiles/me">Go to my passport</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,7 +234,8 @@ const ProfilesDirectory = () => {
|
||||||
No passports found
|
No passports found
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm text-slate-300">
|
<p className="mt-2 text-sm text-slate-300">
|
||||||
Try adjusting your search or realm filters. New members join AeThex every day!
|
Try adjusting your search or realm filters. New members join
|
||||||
|
AeThex every day!
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue