diff --git a/client/pages/ProfilePassport.tsx b/client/pages/ProfilePassport.tsx new file mode 100644 index 00000000..b5ae02ed --- /dev/null +++ b/client/pages/ProfilePassport.tsx @@ -0,0 +1,305 @@ +import { useEffect, useMemo, useState } from "react"; +import { useNavigate, useParams, Link } from "react-router-dom"; +import Layout from "@/components/Layout"; +import LoadingScreen from "@/components/LoadingScreen"; +import PassportSummary from "@/components/passport/PassportSummary"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + aethexUserService, + aethexAchievementService, + aethexProjectService, + type AethexUserProfile, + type AethexAchievement, +} from "@/lib/aethex-database-adapter"; +import { useAuth } from "@/contexts/AuthContext"; +import FourOhFourPage from "@/pages/404"; +import { Clock, Rocket, Target, ExternalLink, Award } from "lucide-react"; + +interface ProjectPreview { + id: string; + title: string; + status: string; + description?: string | null; + created_at?: string; +} + +const formatDate = (value?: string | null) => { + if (!value) return "Recent"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "Recent"; + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + }).format(date); +}; + +const ProfilePassport = () => { + const params = useParams<{ id?: string }>(); + const navigate = useNavigate(); + const { user, linkedProviders } = useAuth(); + + const [profile, setProfile] = useState<(AethexUserProfile & { email?: string | null }) | null>(null); + const [achievements, setAchievements] = useState([]); + const [projects, setProjects] = useState([]); + const [interests, setInterests] = useState([]); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + + const targetUserId = useMemo(() => { + if (params.id === "me" || !params.id) { + return user?.id ?? null; + } + return params.id; + }, [params.id, user?.id]); + + const isSelf = user?.id && profile?.id ? user.id === profile.id : false; + + useEffect(() => { + if (!targetUserId) { + if (!user) { + navigate("/login"); + } + setLoading(false); + return; + } + + const loadProfile = async () => { + try { + setLoading(true); + const [profileData, achievementList, interestList, projectList] = await Promise.all([ + params.id === "me" && profile && profile.id === targetUserId + ? Promise.resolve(profile) + : aethexUserService.getProfileById(targetUserId), + aethexAchievementService.getUserAchievements(targetUserId), + aethexUserService.getUserInterests(targetUserId), + aethexProjectService.getUserProjects(targetUserId).catch(() => []), + ]); + + if (!profileData) { + setNotFound(true); + return; + } + + setProfile(profileData as any); + setAchievements(achievementList ?? []); + setInterests(interestList ?? []); + setProjects( + (projectList ?? []).slice(0, 4).map((project: any) => ({ + id: project.id, + title: project.title, + status: project.status, + description: project.description, + created_at: project.created_at, + })), + ); + setNotFound(false); + } catch (error) { + console.error("Failed to load passport", error); + setNotFound(true); + } finally { + setLoading(false); + } + }; + + loadProfile(); + // We intentionally exclude profile from dependencies to avoid refetch loops when local state updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [targetUserId, params.id]); + + if (loading) { + return ; + } + + if (notFound || !profile) { + return ; + } + + const passportInterests = interests.length + ? interests + : ((profile as any)?.interests as string[]) || []; + + return ( + +
+
+ + + {projects.length > 0 && ( +
+
+
+

Highlighted missions

+

+ A snapshot of what this creator has shipped inside AeThex. +

+
+ {isSelf && ( + + )} +
+
+ {projects.map((project) => ( + + +
+ + {project.title} + + + {project.description || "AeThex project"} + +
+ + {project.status?.replace("_", " ") ?? "active"} + +
+ + + {formatDate(project.created_at)} + + + +
+ ))} +
+
+ )} + +
+
+
+

Achievements

+

+ Passport stamps earned across AeThex experiences. +

+
+ {isSelf && ( + + {achievements.length} badges + + )} +
+ {achievements.length === 0 ? ( + + + No achievements yet. Complete onboarding and participate in missions to earn AeThex badges. + + ) : ( +
+ {achievements.map((achievement) => ( + + +
+ + {achievement.icon || "🏅"} + +
+

+ {achievement.name} +

+

+ {achievement.description || "AeThex honor"} +

+
+
+
+ XP Reward • {achievement.xp_reward ?? 0} + + Passport stamped + +
+
+
+ ))} +
+ )} +
+ + + +
+
+
+

Stay connected

+

+ Reach out, collaborate, and shape the next AeThex release together. +

+
+ {isSelf ? ( + + ) : ( + + )} +
+
+ {profile.github_url && ( + + )} + {profile.linkedin_url && ( + + )} + {profile.twitter_url && ( + + )} + {profile.website_url && ( + + )} + {profile.bio && ( + + {profile.bio} + + )} +
+
+
+
+
+ ); +}; + +export default ProfilePassport;