Add public interactive Roadmap page with gamified dripfeed

cgen-c4bdbe4b48e5464b81d3c6d357e19a75
This commit is contained in:
Builder.io 2025-10-18 23:49:38 +00:00
parent ddc67c3793
commit 16dddfd23c

227
client/pages/Roadmap.tsx Normal file
View file

@ -0,0 +1,227 @@
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 { cn } from "@/lib/utils";
import { useEffect, useMemo, useState } from "react";
import { Sparkles, Lock, Gift, Rocket, Target, Flame, Eye, CheckCircle2, TimerReset } from "lucide-react";
interface Quest {
id: string;
title: string;
xp: number;
phase: "now" | "month1" | "month2" | "month3";
description: string;
}
interface Peek {
id: string;
title: string;
phase: Quest["phase"];
teaser: string;
image?: string;
}
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." },
];
const PEEKS: Peek[] = [
{
id: "peek-1",
title: "Squads mode UI",
phase: "month1",
teaser: "Form elite pods, tackle quests, earn badges.",
image:
"https://images.unsplash.com/photo-1518779578993-ec3579fee39f?q=80&w=1600&auto=format&fit=crop",
},
{
id: "peek-2",
title: "Realtime collab canvas",
phase: "month2",
teaser: "Sketch systems and flows together, live.",
image:
"https://images.unsplash.com/photo-1542831371-29b0f74f9713?q=80&w=1600&auto=format&fit=crop",
},
{
id: "peek-3",
title: "Mentor marketplace",
phase: "month3",
teaser: "Book sessions, verified tracks, loot drops.",
image:
"https://images.unsplash.com/photo-1553877522-43269d4ea984?q=80&w=1600&auto=format&fit=crop",
},
];
const storageKey = "aethex_roadmap_unlocks_v1";
export default function Roadmap() {
const [claimed, setClaimed] = useState<Record<string, boolean>>({});
const [unlocked, setUnlocked] = useState<Record<string, boolean>>({});
useEffect(() => {
try {
const raw = localStorage.getItem(storageKey);
if (raw) setUnlocked(JSON.parse(raw));
} catch {}
}, []);
useEffect(() => {
try {
localStorage.setItem(storageKey, JSON.stringify(unlocked));
} catch {}
}, [unlocked]);
const totalXp = useMemo(() => QUESTS.reduce((s, q) => s + q.xp, 0), []);
const earnedXp = useMemo(
() => QUESTS.reduce((s, q) => s + (claimed[q.id] ? q.xp : 0), 0),
[claimed],
);
const progress = Math.min(100, Math.round((earnedXp / totalXp) * 100));
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 };
return (
<Layout>
<div className="bg-aethex-gradient">
<section className="container mx-auto px-4 pt-14 pb-8">
<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. We drip new content like Rockstar DLC.</p>
</div>
<div className="rounded-lg border border-border/40 bg-background/60 p-4 backdrop-blur">
<div className="flex items-center justify-between gap-4 flex-wrap">
<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>
<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>
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm">
<a href="/changelog">Patch notes</a>
</Button>
<Button asChild size="sm">
<a href="#sneak-peeks">
Sneak peeks <Sparkles className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
</div>
</div>
</section>
{/* Phases */}
<section className="container mx-auto px-4 pb-8">
<div className="grid gap-6 md:grid-cols-2">
{["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";
return (
<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" />
<CardTitle>{title}</CardTitle>
</div>
<CardDescription>{items.length} quests</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{items.map((q) => (
<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>
<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>
</div>
</div>
</div>
))}
</CardContent>
</Card>
);
})}
</div>
</section>
{/* Sneak peeks */}
<section id="sneak-peeks" className="container mx-auto px-4 pb-16">
<div className="mb-4">
<Badge variant="outline" className="border-purple-500/40 text-purple-300">Sneak peeks</Badge>
<h2 className="mt-2 text-2xl font-bold">Rockstar dripfeed</h2>
<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">
<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]")}
loading="lazy"
/>
{!unlocked[p.id] && (
<div className="absolute inset-0 grid place-items-center bg-background/40 backdrop-blur-sm">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Lock className="h-4 w-4" /> Locked
</div>
</div>
)}
</div>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<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>
</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>
<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>
<Button size="sm" asChild>
<a href="/community#spotlight">Follow</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</section>
</div>
</Layout>
);
}