Create ProfilePassport page

cgen-84f72dbce74d4bf5b8c12ec51bcd6fe0
This commit is contained in:
Builder.io 2025-09-30 22:07:01 +00:00
parent 9d4e18c269
commit 57fb1e8119

View file

@ -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<AethexAchievement[]>([]);
const [projects, setProjects] = useState<ProjectPreview[]>([]);
const [interests, setInterests] = useState<string[]>([]);
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 <LoadingScreen message="Loading AeThex passport..." />;
}
if (notFound || !profile) {
return <FourOhFourPage />;
}
const passportInterests = interests.length
? interests
: ((profile as any)?.interests as string[]) || [];
return (
<Layout>
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 py-12">
<div className="container mx-auto px-4 max-w-5xl space-y-10">
<PassportSummary
profile={profile}
achievements={achievements}
interests={passportInterests}
isSelf={isSelf}
linkedProviders={isSelf ? linkedProviders : undefined}
/>
{projects.length > 0 && (
<section className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-xl font-semibold text-white">Highlighted missions</h2>
<p className="text-sm text-slate-300">
A snapshot of what this creator has shipped inside AeThex.
</p>
</div>
{isSelf && (
<Button asChild variant="outline" className="border-slate-700/70 text-slate-100">
<Link to="/projects/new">Launch new project</Link>
</Button>
)}
</div>
<div className="grid gap-4 md:grid-cols-2">
{projects.map((project) => (
<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">
<div className="space-y-1">
<CardTitle className="text-lg text-white">
{project.title}
</CardTitle>
<CardDescription className="text-slate-300">
{project.description || "AeThex project"}
</CardDescription>
</div>
<Badge variant="outline" className="border-slate-700/70 text-slate-200">
{project.status?.replace("_", " ") ?? "active"}
</Badge>
</CardHeader>
<CardContent className="flex items-center justify-between text-xs text-slate-300">
<span className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" /> {formatDate(project.created_at)}
</span>
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-aethex-200">
<Link to="/projects/new">
View mission
<ExternalLink className="ml-1 h-3.5 w-3.5" />
</Link>
</Button>
</CardContent>
</Card>
))}
</div>
</section>
)}
<section className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-white">Achievements</h2>
<p className="text-sm text-slate-300">
Passport stamps earned across AeThex experiences.
</p>
</div>
{isSelf && (
<Badge variant="outline" className="border-aethex-500/50 text-aethex-200">
<Award className="mr-1 h-3 w-3" /> {achievements.length} badges
</Badge>
)}
</div>
{achievements.length === 0 ? (
<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" />
No achievements yet. Complete onboarding and participate in missions to earn AeThex badges.
</Card>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{achievements.map((achievement) => (
<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">
<div className="flex items-center gap-3 text-white">
<span className="text-3xl">
{achievement.icon || "🏅"}
</span>
<div>
<h3 className="text-lg font-semibold">
{achievement.name}
</h3>
<p className="text-sm text-slate-300">
{achievement.description || "AeThex honor"}
</p>
</div>
</div>
<div className="flex items-center justify-between text-xs text-slate-400">
<span>XP Reward {achievement.xp_reward ?? 0}</span>
<span className="flex items-center gap-1 text-aethex-200">
<Rocket className="h-3.5 w-3.5" /> Passport stamped
</span>
</div>
</CardContent>
</Card>
))}
</div>
)}
</section>
<Separator className="border-slate-800" />
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-xl font-semibold text-white">Stay connected</h2>
<p className="text-sm text-slate-300">
Reach out, collaborate, and shape the next AeThex release together.
</p>
</div>
{isSelf ? (
<Button 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 asChild variant="outline" className="border-slate-700/70 text-slate-100">
<Link to="/dashboard">Invite to collaborate</Link>
</Button>
)}
</div>
<div className="flex flex-wrap gap-2 text-sm text-slate-300">
{profile.github_url && (
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
<a href={profile.github_url} target="_blank" rel="noreferrer">
GitHub
<ExternalLink className="ml-1 h-3.5 w-3.5" />
</a>
</Button>
)}
{profile.linkedin_url && (
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
<a href={profile.linkedin_url} target="_blank" rel="noreferrer">
LinkedIn
<ExternalLink className="ml-1 h-3.5 w-3.5" />
</a>
</Button>
)}
{profile.twitter_url && (
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
<a href={profile.twitter_url} target="_blank" rel="noreferrer">
X / Twitter
<ExternalLink className="ml-1 h-3.5 w-3.5" />
</a>
</Button>
)}
{profile.website_url && (
<Button asChild variant="ghost" className="h-8 px-2 text-xs text-slate-200">
<a href={profile.website_url} target="_blank" rel="noreferrer">
Portfolio
<ExternalLink className="ml-1 h-3.5 w-3.5" />
</a>
</Button>
)}
{profile.bio && (
<Badge variant="outline" className="border-slate-700/70 text-slate-200">
{profile.bio}
</Badge>
)}
</div>
</section>
</div>
</div>
</Layout>
);
};
export default ProfilePassport;