Prettier format pending files

This commit is contained in:
Builder.io 2025-10-19 00:29:17 +00:00
parent c87f582775
commit 0d41408ce0
10 changed files with 616 additions and 120 deletions

View file

@ -1,11 +1,31 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CheckCircle2, ExternalLink, Flag, ListChecks, Rocket, Sparkles, Target } from "lucide-react";
import {
CheckCircle2,
ExternalLink,
Flag,
ListChecks,
Rocket,
Sparkles,
Target,
} from "lucide-react";
const linearProjectUrl = "https://linear.app/duo-simulators/project/aethex-roadmap-8600b796e8ad";
const linearProjectUrl =
"https://linear.app/duo-simulators/project/aethex-roadmap-8600b796e8ad";
const phases: { title: string; timeframe: string; items: string[]; icon: any }[] = [
const phases: {
title: string;
timeframe: string;
items: string[];
icon: any;
}[] = [
{
title: "Access & IA",
timeframe: "Now — 02 weeks",
@ -65,11 +85,18 @@ export default function AdminRoadmap() {
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Badge variant="outline" className="border-emerald-500/40 text-emerald-300">
<Badge
variant="outline"
className="border-emerald-500/40 text-emerald-300"
>
All systems focused
</Badge>
<Button asChild size="sm">
<a href={linearProjectUrl} target="_blank" rel="noreferrer noopener">
<a
href={linearProjectUrl}
target="_blank"
rel="noreferrer noopener"
>
Open Linear project <ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
@ -80,7 +107,10 @@ export default function AdminRoadmap() {
{phases.map((p) => {
const Icon = p.icon;
return (
<Card key={p.title} className="bg-card/60 border-border/40 backdrop-blur">
<Card
key={p.title}
className="bg-card/60 border-border/40 backdrop-blur"
>
<CardHeader>
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-aethex-300" />

View file

@ -1,32 +1,101 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Trophy, Award, Crown, Compass } from "lucide-react";
export default function Achievements({ earnedXp, phaseClaims }: { earnedXp: number; phaseClaims: Record<string, number>; }) {
const badges: { id: string; label: string; unlocked: boolean; icon: any; desc: string }[] = [
{ id: "rookie", label: "Rookie Explorer", unlocked: earnedXp >= 100, icon: Compass, desc: "Earn 100+ XP" },
{ id: "seasoned", label: "Seasoned Adventurer", unlocked: earnedXp >= 300, icon: Trophy, desc: "Earn 300+ XP" },
{ id: "master", label: "Master Builder", unlocked: earnedXp >= 600, icon: Crown, desc: "Earn 600+ XP" },
{ id: "trailblazer", label: "Trailblazer", unlocked: ["now","month1","month2","month3"].every((k) => (phaseClaims[k] || 0) > 0), icon: Award, desc: "Complete a quest in every phase" },
export default function Achievements({
earnedXp,
phaseClaims,
}: {
earnedXp: number;
phaseClaims: Record<string, number>;
}) {
const badges: {
id: string;
label: string;
unlocked: boolean;
icon: any;
desc: string;
}[] = [
{
id: "rookie",
label: "Rookie Explorer",
unlocked: earnedXp >= 100,
icon: Compass,
desc: "Earn 100+ XP",
},
{
id: "seasoned",
label: "Seasoned Adventurer",
unlocked: earnedXp >= 300,
icon: Trophy,
desc: "Earn 300+ XP",
},
{
id: "master",
label: "Master Builder",
unlocked: earnedXp >= 600,
icon: Crown,
desc: "Earn 600+ XP",
},
{
id: "trailblazer",
label: "Trailblazer",
unlocked: ["now", "month1", "month2", "month3"].every(
(k) => (phaseClaims[k] || 0) > 0,
),
icon: Award,
desc: "Complete a quest in every phase",
},
];
return (
<Card className="bg-card/60 border-border/40 backdrop-blur">
<CardHeader>
<CardTitle>Achievements</CardTitle>
<CardDescription>Earn badges as you progress. Displayed publicly soon.</CardDescription>
<CardDescription>
Earn badges as you progress. Displayed publicly soon.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-3">
{badges.map((b) => {
const Icon = b.icon;
return (
<div key={b.id} className="flex items-center gap-2 rounded border border-border/40 px-3 py-2">
<Icon className={"h-4 w-4 " + (b.unlocked ? "text-aethex-300" : "text-muted-foreground")} />
<div
key={b.id}
className="flex items-center gap-2 rounded border border-border/40 px-3 py-2"
>
<Icon
className={
"h-4 w-4 " +
(b.unlocked ? "text-aethex-300" : "text-muted-foreground")
}
/>
<div className="text-sm">
<div className={b.unlocked ? "text-foreground" : "text-muted-foreground"}>{b.label}</div>
<div className="text-[11px] text-muted-foreground">{b.desc}</div>
<div
className={
b.unlocked ? "text-foreground" : "text-muted-foreground"
}
>
{b.label}
</div>
<div className="text-[11px] text-muted-foreground">
{b.desc}
</div>
</div>
<Badge variant="outline" className={b.unlocked ? "ml-2 border-emerald-500/40 text-emerald-300" : "ml-2"}>
<Badge
variant="outline"
className={
b.unlocked
? "ml-2 border-emerald-500/40 text-emerald-300"
: "ml-2"
}
>
{b.unlocked ? "Unlocked" : "Locked"}
</Badge>
</div>

View file

@ -8,7 +8,13 @@ export interface PhaseSummary {
percent: number; // 0..100
}
export default function GalaxyMap({ phases, onSelect }: { phases: PhaseSummary[]; onSelect?: (id: PhaseSummary["id"]) => void }) {
export default function GalaxyMap({
phases,
onSelect,
}: {
phases: PhaseSummary[];
onSelect?: (id: PhaseSummary["id"]) => void;
}) {
const iconFor: Record<PhaseSummary["id"], any> = {
now: Target,
month1: Flame,
@ -18,8 +24,15 @@ export default function GalaxyMap({ phases, onSelect }: { phases: PhaseSummary[]
return (
<div className="relative rounded-xl border border-border/40 bg-background/50 p-6 overflow-hidden">
<div className="pointer-events-none absolute inset-0 opacity-30" aria-hidden>
<svg className="h-full w-full" viewBox="0 0 800 240" preserveAspectRatio="none">
<div
className="pointer-events-none absolute inset-0 opacity-30"
aria-hidden
>
<svg
className="h-full w-full"
viewBox="0 0 800 240"
preserveAspectRatio="none"
>
<defs>
<radialGradient id="g" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="rgba(99,102,241,0.35)" />
@ -36,18 +49,35 @@ export default function GalaxyMap({ phases, onSelect }: { phases: PhaseSummary[]
{phases.map((p) => {
const Icon = iconFor[p.id];
return (
<button key={p.id} onClick={() => onSelect?.(p.id)} className="group relative rounded-xl border border-border/40 bg-background/60 p-4 backdrop-blur hover:border-aethex-400/50 transition">
<button
key={p.id}
onClick={() => onSelect?.(p.id)}
className="group relative rounded-xl border border-border/40 bg-background/60 p-4 backdrop-blur hover:border-aethex-400/50 transition"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-aethex-300" />
<span className="font-semibold text-foreground">{p.label}</span>
<span className="font-semibold text-foreground">
{p.label}
</span>
</div>
<Badge variant="outline" className="text-xs">{p.percent}%</Badge>
<Badge variant="outline" className="text-xs">
{p.percent}%
</Badge>
</div>
<div className="mt-3 h-2 w-full rounded bg-border/50 overflow-hidden">
<div className={cn("h-full bg-gradient-to-r from-aethex-500 to-neon-blue transition-all" )} style={{ width: `${Math.max(0, Math.min(100, Math.round(p.percent))) }%` }} />
<div
className={cn(
"h-full bg-gradient-to-r from-aethex-500 to-neon-blue transition-all",
)}
style={{
width: `${Math.max(0, Math.min(100, Math.round(p.percent)))}%`,
}}
/>
</div>
<div className="mt-2 text-xs text-muted-foreground">
Tap to focus this phase
</div>
<div className="mt-2 text-xs text-muted-foreground">Tap to focus this phase</div>
</button>
);
})}
@ -55,7 +85,9 @@ export default function GalaxyMap({ phases, onSelect }: { phases: PhaseSummary[]
<div className="mt-3 flex items-center gap-2">
<Waypoints className="h-4 w-4 text-muted-foreground" />
<p className="text-xs text-muted-foreground">Planets represent phases. Progress fills as quests are claimed.</p>
<p className="text-xs text-muted-foreground">
Planets represent phases. Progress fills as quests are claimed.
</p>
</div>
</div>
);

View file

@ -5,7 +5,13 @@ export type RoadmapTheme = "space" | "fantasy" | "city" | "adventure";
const key = "aethex_roadmap_theme_v1";
export default function ThemeToggle({ value, onChange }: { value?: RoadmapTheme; onChange?: (v: RoadmapTheme) => void }) {
export default function ThemeToggle({
value,
onChange,
}: {
value?: RoadmapTheme;
onChange?: (v: RoadmapTheme) => void;
}) {
const [theme, setTheme] = useState<RoadmapTheme>(value || "space");
useEffect(() => {
@ -29,7 +35,11 @@ export default function ThemeToggle({ value, onChange }: { value?: RoadmapTheme;
};
return (
<Tabs value={theme} onValueChange={(v) => set(v as RoadmapTheme)} className="w-full">
<Tabs
value={theme}
onValueChange={(v) => set(v as RoadmapTheme)}
className="w-full"
>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="space">Space</TabsTrigger>
<TabsTrigger value="fantasy">Fantasy</TabsTrigger>

View file

@ -2,7 +2,14 @@ import { useMemo, useRef } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Rocket, Target, Flame, Sparkles } from "lucide-react";
import {
CheckCircle2,
Circle,
Rocket,
Target,
Flame,
Sparkles,
} from "lucide-react";
export type RoadmapPhase = "now" | "month1" | "month2" | "month3";
@ -25,17 +32,34 @@ export default function Timeline({
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const phases: RoadmapPhase[] = ["now", "month1", "month2", "month3"];
const iconFor: Record<RoadmapPhase, any> = { now: Target, month1: Flame, month2: Rocket, month3: Sparkles };
const iconFor: Record<RoadmapPhase, any> = {
now: Target,
month1: Flame,
month2: Rocket,
month3: Sparkles,
};
const grouped = useMemo(() => {
const map: Record<RoadmapPhase, TimelineEvent[]> = { now: [], month1: [], month2: [], month3: [] };
const map: Record<RoadmapPhase, TimelineEvent[]> = {
now: [],
month1: [],
month2: [],
month3: [],
};
for (const e of events) map[e.phase].push(e);
return map;
}, [events]);
const scrollToPhase = (p: RoadmapPhase) => {
const el = containerRef.current?.querySelector<HTMLDivElement>(`[data-phase="${p}"]`);
if (el) el.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
const el = containerRef.current?.querySelector<HTMLDivElement>(
`[data-phase="${p}"]`,
);
if (el)
el.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
onSelectPhase?.(p);
};
@ -45,9 +69,22 @@ export default function Timeline({
<div className="flex items-center gap-2">
{phases.map((p) => {
const Icon = iconFor[p];
const label = p === "now" ? "Now" : p === "month1" ? "Month 1" : p === "month2" ? "Month 2" : "Month 3";
const label =
p === "now"
? "Now"
: p === "month1"
? "Month 1"
: p === "month2"
? "Month 2"
: "Month 3";
return (
<Button key={p} size="sm" variant="outline" onClick={() => scrollToPhase(p)} className="flex items-center gap-2">
<Button
key={p}
size="sm"
variant="outline"
onClick={() => scrollToPhase(p)}
className="flex items-center gap-2"
>
<Icon className="h-4 w-4 text-aethex-300" /> {label}
</Button>
);
@ -64,9 +101,18 @@ export default function Timeline({
<div key={p} data-phase={p} className="col-span-3">
{/* Phase header */}
<div className="mb-2 flex items-center gap-2">
{(() => { const Icon = iconFor[p]; return <Icon className="h-4 w-4 text-aethex-300" />; })()}
{(() => {
const Icon = iconFor[p];
return <Icon className="h-4 w-4 text-aethex-300" />;
})()}
<span className="text-sm font-medium">
{p === "now" ? "Now" : p === "month1" ? "Month 1" : p === "month2" ? "Month 2" : "Month 3"}
{p === "now"
? "Now"
: p === "month1"
? "Month 1"
: p === "month2"
? "Month 2"
: "Month 3"}
</span>
</div>
{/* Events for phase */}
@ -90,15 +136,23 @@ export default function Timeline({
</div>
<div className="flex items-start justify-between gap-2">
<div>
<div className="text-sm font-medium leading-tight">{e.title}</div>
<div className="text-xs text-muted-foreground">Tap to {e.claimed ? "unclaim" : "claim"} {e.xp} XP</div>
<div className="text-sm font-medium leading-tight">
{e.title}
</div>
<div className="text-xs text-muted-foreground">
Tap to {e.claimed ? "unclaim" : "claim"} {e.xp} XP
</div>
</div>
<Badge variant="outline" className="shrink-0">{e.xp} XP</Badge>
<Badge variant="outline" className="shrink-0">
{e.xp} XP
</Badge>
</div>
</button>
))}
{grouped[p].length === 0 && (
<div className="rounded border border-border/40 p-3 text-xs text-muted-foreground">No quests yet</div>
<div className="rounded border border-border/40 p-3 text-xs text-muted-foreground">
No quests yet
</div>
)}
</div>
</div>

View file

@ -1,9 +1,19 @@
import { useEffect, useMemo, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
export default function VoteWidget({ options }: { options: { id: string; label: string }[] }) {
export default function VoteWidget({
options,
}: {
options: { id: string; label: string }[];
}) {
const key = "aethex_roadmap_votes_v1";
const [votes, setVotes] = useState<Record<string, number>>({});
const [choice, setChoice] = useState<string | null>(null);
@ -21,7 +31,10 @@ export default function VoteWidget({ options }: { options: { id: string; label:
} catch {}
}, [votes]);
const total = useMemo(() => Object.values(votes).reduce((a, b) => a + b, 0), [votes]);
const total = useMemo(
() => Object.values(votes).reduce((a, b) => a + b, 0),
[votes],
);
const vote = (id: string) => {
setChoice(id);
@ -37,23 +50,37 @@ export default function VoteWidget({ options }: { options: { id: string; label:
<Card className="bg-card/60 border-border/40 backdrop-blur">
<CardHeader>
<CardTitle>What should we ship next?</CardTitle>
<CardDescription>Local voting preview. Public voting will sync later.</CardDescription>
<CardDescription>
Local voting preview. Public voting will sync later.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-2 md:grid-cols-2">
{options.map((o) => {
const pct = total ? Math.round(((votes[o.id] || 0) / total) * 100) : 0;
const pct = total
? Math.round(((votes[o.id] || 0) / total) * 100)
: 0;
return (
<div key={o.id} className="flex items-center justify-between rounded border border-border/40 p-3">
<div
key={o.id}
className="flex items-center justify-between rounded border border-border/40 p-3"
>
<div>
<div className="text-sm font-medium">{o.label}</div>
<div className="mt-1 h-1.5 w-40 rounded bg-border/50 overflow-hidden">
<div className="h-full bg-gradient-to-r from-aethex-500 to-neon-blue" style={{ width: `${pct}%` }} />
<div
className="h-full bg-gradient-to-r from-aethex-500 to-neon-blue"
style={{ width: `${pct}%` }}
/>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{pct}%</Badge>
<Button size="sm" variant={choice === o.id ? "outline" : "default"} onClick={() => vote(o.id)}>
<Button
size="sm"
variant={choice === o.id ? "outline" : "default"}
onClick={() => vote(o.id)}
>
{choice === o.id ? "Voted" : "Vote"}
</Button>
</div>
@ -62,8 +89,12 @@ export default function VoteWidget({ options }: { options: { id: string; label:
})}
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={reset}>Reset votes</Button>
<div className="text-xs text-muted-foreground">Votes are stored locally on your device.</div>
<Button variant="outline" size="sm" onClick={reset}>
Reset votes
</Button>
<div className="text-xs text-muted-foreground">
Votes are stored locally on your device.
</div>
</div>
</CardContent>
</Card>

View file

@ -99,9 +99,15 @@
}
@layer utilities {
.section-cozy { padding-block: var(--space-section-y); }
.gap-cozy { gap: var(--space-5); }
.pad-cozy { padding: var(--space-5); }
.section-cozy {
padding-block: var(--space-section-y);
}
.gap-cozy {
gap: var(--space-5);
}
.pad-cozy {
padding: var(--space-5);
}
.text-gradient {
@apply bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600 bg-clip-text text-transparent;

View file

@ -1,10 +1,26 @@
import Layout from "@/components/Layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { useEffect, useMemo, useState } from "react";
import { Sparkles, Lock, Gift, Rocket, Target, Flame, Eye, CheckCircle2, TimerReset } from "lucide-react";
import {
Sparkles,
Lock,
Gift,
Rocket,
Target,
Flame,
Eye,
CheckCircle2,
TimerReset,
} from "lucide-react";
import Timeline from "@/components/roadmap/Timeline";
import GalaxyMap from "@/components/roadmap/GalaxyMap";
import Achievements from "@/components/roadmap/Achievements";
@ -27,18 +43,90 @@ interface Peek {
}
const QUESTS: Quest[] = [
{ id: "realm-gating", title: "Finalize realm gating", xp: 120, phase: "now", description: "Tight access across routes with glossy redirects." },
{ id: "nav-ia", title: "Unify navigation", xp: 80, phase: "now", description: "Consistent top-level labels, fewer detours." },
{ id: "mentor-admin", title: "Mentorship admin flows", xp: 100, phase: "now", description: "Accept / reject, filters, analytics." },
{ id: "mentor-polish", title: "Mentor directory polish", xp: 120, phase: "month1", description: "Filters, profiles, and smoother requests." },
{ id: "featured-studios", title: "Featured studios persistence", xp: 90, phase: "month1", description: "Curation + partner spotlight." },
{ id: "blog-seo", title: "Blog editor + SEO", xp: 130, phase: "month1", description: "Meta/OG, list pages, Supabase sync." },
{ id: "opps-tooling", title: "Opportunities tooling", xp: 140, phase: "month2", description: "Applicant review, statuses, filters." },
{ id: "pricing-funnels", title: "Engage/pricing funnels", xp: 120, phase: "month2", description: "Plans and conversion events." },
{ id: "observability", title: "Observability + Sentry", xp: 110, phase: "month2", description: "Errors, alerts, status surfacing." },
{ id: "collab-teams", title: "Teams & projects enhancements", xp: 150, phase: "month3", description: "Membership, board UX, notifications." },
{ id: "advanced-mentoring", title: "Advanced mentoring flows", xp: 150, phase: "month3", description: "Availability, pricing, scheduling hooks." },
{ id: "public-roadmap", title: "Public roadmap page", xp: 100, phase: "month3", description: "This page—interactive and fun." },
{
id: "realm-gating",
title: "Finalize realm gating",
xp: 120,
phase: "now",
description: "Tight access across routes with glossy redirects.",
},
{
id: "nav-ia",
title: "Unify navigation",
xp: 80,
phase: "now",
description: "Consistent top-level labels, fewer detours.",
},
{
id: "mentor-admin",
title: "Mentorship admin flows",
xp: 100,
phase: "now",
description: "Accept / reject, filters, analytics.",
},
{
id: "mentor-polish",
title: "Mentor directory polish",
xp: 120,
phase: "month1",
description: "Filters, profiles, and smoother requests.",
},
{
id: "featured-studios",
title: "Featured studios persistence",
xp: 90,
phase: "month1",
description: "Curation + partner spotlight.",
},
{
id: "blog-seo",
title: "Blog editor + SEO",
xp: 130,
phase: "month1",
description: "Meta/OG, list pages, Supabase sync.",
},
{
id: "opps-tooling",
title: "Opportunities tooling",
xp: 140,
phase: "month2",
description: "Applicant review, statuses, filters.",
},
{
id: "pricing-funnels",
title: "Engage/pricing funnels",
xp: 120,
phase: "month2",
description: "Plans and conversion events.",
},
{
id: "observability",
title: "Observability + Sentry",
xp: 110,
phase: "month2",
description: "Errors, alerts, status surfacing.",
},
{
id: "collab-teams",
title: "Teams & projects enhancements",
xp: 150,
phase: "month3",
description: "Membership, board UX, notifications.",
},
{
id: "advanced-mentoring",
title: "Advanced mentoring flows",
xp: 150,
phase: "month3",
description: "Availability, pricing, scheduling hooks.",
},
{
id: "public-roadmap",
title: "Public roadmap page",
xp: 100,
phase: "month3",
description: "This page—interactive and fun.",
},
];
const PEEKS: Peek[] = [
@ -96,7 +184,10 @@ export default function Roadmap() {
const progress = Math.min(100, Math.round((earnedXp / totalXp) * 100));
const phaseTotals = useMemo(() => {
const res: Record<string, { total: number; earned: number; count: number }> = {};
const res: Record<
string,
{ total: number; earned: number; count: number }
> = {};
for (const q of QUESTS) {
const key = q.phase;
res[key] = res[key] || { total: 0, earned: 0, count: 0 };
@ -115,19 +206,36 @@ export default function Roadmap() {
return res;
}, [claimed]);
const toggleClaim = (id: string) => setClaimed((m) => ({ ...m, [id]: !m[id] }));
const toggleUnlock = (id: string) => setUnlocked((m) => ({ ...m, [id]: !m[id] }));
const toggleClaim = (id: string) =>
setClaimed((m) => ({ ...m, [id]: !m[id] }));
const toggleUnlock = (id: string) =>
setUnlocked((m) => ({ ...m, [id]: !m[id] }));
const PhaseIcon: Record<string, any> = { now: Target, month1: Flame, month2: Rocket, month3: Sparkles };
const PhaseIcon: Record<string, any> = {
now: Target,
month1: Flame,
month2: Rocket,
month3: Sparkles,
};
return (
<Layout>
<div className="bg-aethex-gradient">
<section className="container mx-auto max-w-7xl px-4 section-cozy">
<div className="mb-6">
<Badge variant="outline" className="border-aethex-400/50 text-aethex-300">Roadmap</Badge>
<h1 className="mt-2 text-4xl font-extrabold text-gradient">The AeThex Roadmap</h1>
<p className="text-muted-foreground max-w-2xl mt-1">Follow along, earn XP, and unlock sneak peeks. New drops roll out regularly.</p>
<Badge
variant="outline"
className="border-aethex-400/50 text-aethex-300"
>
Roadmap
</Badge>
<h1 className="mt-2 text-4xl font-extrabold text-gradient">
The AeThex Roadmap
</h1>
<p className="text-muted-foreground max-w-2xl mt-1">
Follow along, earn XP, and unlock sneak peeks. New drops roll out
regularly.
</p>
</div>
<div className="rounded-lg border border-border/40 bg-background/60 p-4 backdrop-blur">
@ -135,15 +243,28 @@ export default function Roadmap() {
<div className="min-w-[240px]">
<p className="text-sm text-muted-foreground">Community XP</p>
<div className="mt-2 h-2 w-full rounded bg-border/50 overflow-hidden">
<div className="h-full bg-gradient-to-r from-aethex-500 to-neon-blue" style={{ width: `${progress}%` }} />
<div
className="h-full bg-gradient-to-r from-aethex-500 to-neon-blue"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-1 text-xs text-muted-foreground">{earnedXp} / {totalXp} XP</p>
<p className="mt-1 text-xs text-muted-foreground">
{earnedXp} / {totalXp} XP
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="capitalize">Now</Badge>
<Badge variant="outline" className="capitalize">Next</Badge>
<Badge variant="outline" className="capitalize">Later</Badge>
<Badge className="bg-gradient-to-r from-aethex-500 to-neon-blue">Legendary</Badge>
<Badge variant="outline" className="capitalize">
Now
</Badge>
<Badge variant="outline" className="capitalize">
Next
</Badge>
<Badge variant="outline" className="capitalize">
Later
</Badge>
<Badge className="bg-gradient-to-r from-aethex-500 to-neon-blue">
Legendary
</Badge>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm">
@ -162,15 +283,32 @@ export default function Roadmap() {
<section className="container mx-auto max-w-7xl px-4 section-cozy">
<div className="grid gap-6 md:grid-cols-[1fr_1fr]">
<GalaxyMap
phases={["now","month1","month2","month3"].map((id) => ({
phases={["now", "month1", "month2", "month3"].map((id) => ({
id: id as Quest["phase"],
label: id === "now" ? "Now" : id === "month1" ? "Month 1" : id === "month2" ? "Month 2" : "Month 3",
percent: phaseTotals[id]?.total ? Math.round((phaseTotals[id].earned / phaseTotals[id].total) * 100) : 0,
label:
id === "now"
? "Now"
: id === "month1"
? "Month 1"
: id === "month2"
? "Month 2"
: "Month 3",
percent: phaseTotals[id]?.total
? Math.round(
(phaseTotals[id].earned / phaseTotals[id].total) * 100,
)
: 0,
}))}
onSelect={(id) => setFocusedPhase(id)}
/>
<Timeline
events={QUESTS.map((q) => ({ id: q.id, title: q.title, phase: q.phase, xp: q.xp, claimed: !!claimed[q.id] }))}
events={QUESTS.map((q) => ({
id: q.id,
title: q.title,
phase: q.phase,
xp: q.xp,
claimed: !!claimed[q.id],
}))}
onSelectPhase={(p) => setFocusedPhase(p)}
onToggleClaim={(id) => toggleClaim(id)}
/>
@ -180,12 +318,25 @@ export default function Roadmap() {
{/* Phases */}
<section className="container mx-auto px-4 pb-8">
<div className="grid gap-6 md:grid-cols-2">
{(focusedPhase ? [focusedPhase] : ["now","month1","month2","month3"]).map((phase) => {
{(focusedPhase
? [focusedPhase]
: ["now", "month1", "month2", "month3"]
).map((phase) => {
const Icon = PhaseIcon[phase] || Target;
const items = QUESTS.filter((q) => q.phase === phase);
const title = phase === "now" ? "Now" : phase === "month1" ? "Month 1" : phase === "month2" ? "Month 2" : "Month 3";
const title =
phase === "now"
? "Now"
: phase === "month1"
? "Month 1"
: phase === "month2"
? "Month 2"
: "Month 3";
return (
<Card key={phase} className="bg-card/60 border-border/40 backdrop-blur">
<Card
key={phase}
className="bg-card/60 border-border/40 backdrop-blur"
>
<CardHeader>
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-aethex-300" />
@ -195,16 +346,32 @@ export default function Roadmap() {
</CardHeader>
<CardContent className="space-y-3">
{items.map((q) => (
<div key={q.id} className="rounded border border-border/40 p-3">
<div
key={q.id}
className="rounded border border-border/40 p-3"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium">{q.title}</div>
<div className="text-xs text-muted-foreground">{q.description}</div>
<div className="text-xs text-muted-foreground">
{q.description}
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{q.xp} XP</Badge>
<Button size="sm" onClick={() => toggleClaim(q.id)} variant={claimed[q.id] ? "outline" : "default"}>
{claimed[q.id] ? <><CheckCircle2 className="h-4 w-4 mr-1" /> Claimed</> : "Claim"}
<Button
size="sm"
onClick={() => toggleClaim(q.id)}
variant={claimed[q.id] ? "outline" : "default"}
>
{claimed[q.id] ? (
<>
<CheckCircle2 className="h-4 w-4 mr-1" />{" "}
Claimed
</>
) : (
"Claim"
)}
</Button>
</div>
</div>
@ -222,20 +389,38 @@ export default function Roadmap() {
</section>
{/* Sneak peeks */}
<section id="sneak-peeks" className="container mx-auto max-w-7xl px-4 section-cozy">
<section
id="sneak-peeks"
className="container mx-auto max-w-7xl px-4 section-cozy"
>
<div className="mb-4">
<Badge variant="outline" className="border-purple-500/40 text-purple-300">Sneak peeks</Badge>
<Badge
variant="outline"
className="border-purple-500/40 text-purple-300"
>
Sneak peeks
</Badge>
<h2 className="mt-2 text-2xl font-bold">Dev Drops</h2>
<p className="text-sm text-muted-foreground">Unlock previews as we get closer. Collect them all.</p>
<p className="text-sm text-muted-foreground">
Unlock previews as we get closer. Collect them all.
</p>
</div>
<div className="grid gap-6 md:grid-cols-3">
{PEEKS.map((p) => (
<Card key={p.id} className="bg-card/60 border-border/40 backdrop-blur overflow-hidden group">
<Card
key={p.id}
className="bg-card/60 border-border/40 backdrop-blur overflow-hidden group"
>
<div className="relative h-40 w-full">
<img
src={p.image}
alt={p.title}
className={cn("h-full w-full object-cover transition-all duration-300", unlocked[p.id] ? "" : "blur-sm scale-[1.02] brightness-[0.7]")}
className={cn(
"h-full w-full object-cover transition-all duration-300",
unlocked[p.id]
? ""
: "blur-sm scale-[1.02] brightness-[0.7]",
)}
loading="lazy"
/>
{!unlocked[p.id] && (
@ -251,13 +436,35 @@ export default function Roadmap() {
<Eye className="h-4 w-4 text-aethex-300" />
<CardTitle className="text-base">{p.title}</CardTitle>
</div>
<CardDescription className="line-clamp-2">{p.teaser}</CardDescription>
<CardDescription className="line-clamp-2">
{p.teaser}
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-between gap-2 pt-0 pb-4">
<Badge variant="outline" className="capitalize">{p.phase === "now" ? "Now" : p.phase === "month1" ? "Month 1" : p.phase === "month2" ? "Month 2" : "Month 3"}</Badge>
<Badge variant="outline" className="capitalize">
{p.phase === "now"
? "Now"
: p.phase === "month1"
? "Month 1"
: p.phase === "month2"
? "Month 2"
: "Month 3"}
</Badge>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" onClick={() => toggleUnlock(p.id)}>
{unlocked[p.id] ? <>Hide <TimerReset className="ml-2 h-4 w-4" /></> : <>Unlock <Gift className="ml-2 h-4 w-4" /></>}
<Button
size="sm"
variant="outline"
onClick={() => toggleUnlock(p.id)}
>
{unlocked[p.id] ? (
<>
Hide <TimerReset className="ml-2 h-4 w-4" />
</>
) : (
<>
Unlock <Gift className="ml-2 h-4 w-4" />
</>
)}
</Button>
<Button size="sm" asChild>
<a href="/community#spotlight">Follow</a>
@ -268,7 +475,9 @@ export default function Roadmap() {
))}
</div>
<div className="mt-8">
<VoteWidget options={PEEKS.map((p) => ({ id: p.id, label: p.title }))} />
<VoteWidget
options={PEEKS.map((p) => ({ id: p.id, label: p.title }))}
/>
</div>
</section>
</div>

View file

@ -1,7 +1,13 @@
import Layout from "@/components/Layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { aethexSocialService } from "@/lib/aethex-social-service";
@ -31,8 +37,15 @@ export default function MentorProfile() {
const load = async () => {
setLoading(true);
try {
const rows = (await aethexSocialService.listMentors({ q: username, limit: 50 })) as MentorRow[];
const found = rows.find((r) => (r.user_profiles?.username || "").toLowerCase() === (username || "").toLowerCase());
const rows = (await aethexSocialService.listMentors({
q: username,
limit: 50,
})) as MentorRow[];
const found = rows.find(
(r) =>
(r.user_profiles?.username || "").toLowerCase() ===
(username || "").toLowerCase(),
);
setMentor(found || null);
} catch {
setMentor(null);
@ -43,25 +56,45 @@ export default function MentorProfile() {
load();
}, [username]);
const displayName = useMemo(() => mentor?.user_profiles?.full_name || mentor?.user_profiles?.username || "Mentor", [mentor]);
const displayName = useMemo(
() =>
mentor?.user_profiles?.full_name ||
mentor?.user_profiles?.username ||
"Mentor",
[mentor],
);
return (
<Layout>
<div className="container mx-auto max-w-7xl px-4 py-12">
<div className="mb-6">
<Badge variant="outline" className="mb-2">Mentorship</Badge>
<h1 className="text-3xl font-bold">{loading ? "Loading…" : displayName}</h1>
<Badge variant="outline" className="mb-2">
Mentorship
</Badge>
<h1 className="text-3xl font-bold">
{loading ? "Loading…" : displayName}
</h1>
{!loading && (
<p className="text-muted-foreground mt-1">{mentor?.user_profiles?.bio || mentor?.bio || "Mentor profile"}</p>
<p className="text-muted-foreground mt-1">
{mentor?.user_profiles?.bio || mentor?.bio || "Mentor profile"}
</p>
)}
</div>
{loading && (
<Card><CardContent className="p-6 text-sm text-muted-foreground">Loading profile</CardContent></Card>
<Card>
<CardContent className="p-6 text-sm text-muted-foreground">
Loading profile
</CardContent>
</Card>
)}
{!loading && !mentor && (
<Card><CardContent className="p-6 text-sm text-muted-foreground">Mentor not found.</CardContent></Card>
<Card>
<CardContent className="p-6 text-sm text-muted-foreground">
Mentor not found.
</CardContent>
</Card>
)}
{!loading && mentor && (
@ -72,10 +105,14 @@ export default function MentorProfile() {
<CardDescription>Background and focus areas</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{mentor.bio && <p className="text-sm text-muted-foreground">{mentor.bio}</p>}
{mentor.bio && (
<p className="text-sm text-muted-foreground">{mentor.bio}</p>
)}
<div className="flex flex-wrap gap-2">
{(mentor.expertise || []).map((tag) => (
<Badge key={tag} variant="outline">{tag}</Badge>
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
</CardContent>
@ -88,14 +125,30 @@ export default function MentorProfile() {
</CardHeader>
<CardContent className="space-y-3">
<div className="text-sm">
<div>Availability: {mentor.available ? "Accepting requests" : "Unavailable"}</div>
<div>
Availability:{" "}
{mentor.available ? "Accepting requests" : "Unavailable"}
</div>
{typeof mentor.hourly_rate === "number" && (
<div>Rate: ${mentor.hourly_rate}/hr</div>
)}
</div>
<div className="flex gap-2">
<Button onClick={() => navigate(`/community/mentorship?m=${mentor.user_profiles?.username || mentor.user_id}`)}>Request mentorship</Button>
<Button variant="outline" onClick={() => navigate("/community/mentorship")}>Back to directory</Button>
<Button
onClick={() =>
navigate(
`/community/mentorship?m=${mentor.user_profiles?.username || mentor.user_id}`,
)
}
>
Request mentorship
</Button>
<Button
variant="outline"
onClick={() => navigate("/community/mentorship")}
>
Back to directory
</Button>
</div>
</CardContent>
</Card>

View file

@ -281,7 +281,9 @@ export default function MentorshipRequest() {
onClick={() => {
const uname = m.user_profiles?.username;
if (uname) {
navigate(`/community/mentor/${encodeURIComponent(uname)}`);
navigate(
`/community/mentor/${encodeURIComponent(uname)}`,
);
}
}}
className="w-full"