Adds a new downloads page for desktop applications and integrates new milestones for desktop and mobile applications into the roadmap page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: f42fc35c-b758-4fe4-9d6a-c616c5c45526 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/UJaB6ez Replit-Helium-Checkpoint-Created: true
529 lines
17 KiB
TypeScript
529 lines
17 KiB
TypeScript
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 { useAuth } from "@/contexts/AuthContext";
|
|
import { aethexToast } from "@/lib/aethex-toast";
|
|
import {
|
|
Sparkles,
|
|
Lock,
|
|
Gift,
|
|
Rocket,
|
|
Target,
|
|
Flame,
|
|
Eye,
|
|
CheckCircle2,
|
|
TimerReset,
|
|
} from "lucide-react";
|
|
import GalaxyMap from "@/components/roadmap/GalaxyMap";
|
|
import Achievements from "@/components/roadmap/Achievements";
|
|
import VoteWidget from "@/components/roadmap/VoteWidget";
|
|
|
|
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: "desktop-app",
|
|
title: "Desktop App (Beta)",
|
|
xp: 200,
|
|
phase: "now",
|
|
description: "Windows, macOS, Linux builds with file watcher overlay.",
|
|
},
|
|
{
|
|
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: "desktop-stable",
|
|
title: "Desktop App (Stable)",
|
|
xp: 180,
|
|
phase: "month2",
|
|
description: "Auto-updates, crash reporting, performance tuning.",
|
|
},
|
|
{
|
|
id: "mobile-ios",
|
|
title: "iOS App (Beta)",
|
|
xp: 250,
|
|
phase: "month3",
|
|
description: "Native iOS app with notifications and quick actions.",
|
|
},
|
|
{
|
|
id: "mobile-android",
|
|
title: "Android App (Beta)",
|
|
xp: 250,
|
|
phase: "month3",
|
|
description: "Native Android app with notifications and quick actions.",
|
|
},
|
|
];
|
|
|
|
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",
|
|
},
|
|
{
|
|
id: "peek-4",
|
|
title: "Mobile App Preview",
|
|
phase: "month3",
|
|
teaser: "Native iOS and Android apps with push notifications.",
|
|
image:
|
|
"https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?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>>({});
|
|
const [focusedPhase, setFocusedPhase] = useState<Quest["phase"] | null>(null);
|
|
const { user } = useAuth();
|
|
|
|
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 phaseTotals = useMemo(() => {
|
|
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 };
|
|
res[key].total += q.xp;
|
|
res[key].count += 1;
|
|
if (claimed[q.id]) res[key].earned += q.xp;
|
|
}
|
|
return res;
|
|
}, [claimed]);
|
|
|
|
const phaseClaims: Record<string, number> = useMemo(() => {
|
|
const res: Record<string, number> = {};
|
|
for (const q of QUESTS) {
|
|
if (claimed[q.id]) res[q.phase] = (res[q.phase] || 0) + 1;
|
|
}
|
|
return res;
|
|
}, [claimed]);
|
|
|
|
const toggleClaim = (id: string) =>
|
|
setClaimed((m) => ({ ...m, [id]: !m[id] }));
|
|
const toggleUnlock = (id: string) => {
|
|
if (!user) {
|
|
try {
|
|
aethexToast.info({
|
|
title: "Sign in required",
|
|
description:
|
|
"Create an account to unlock Dev Drops and save progress.",
|
|
});
|
|
} catch {}
|
|
return;
|
|
}
|
|
setUnlocked((m) => ({ ...m, [id]: !m[id] }));
|
|
};
|
|
|
|
const PhaseIcon: Record<string, any> = {
|
|
now: Target,
|
|
month1: Flame,
|
|
month2: Rocket,
|
|
month3: Sparkles,
|
|
};
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="min-h-screen bg-aethex-gradient py-12">
|
|
<section className="container mx-auto max-w-6xl px-4">
|
|
<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>
|
|
</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>
|
|
|
|
<section className="container mx-auto max-w-6xl px-4 mt-8">
|
|
<div className="space-y-6">
|
|
<GalaxyMap
|
|
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,
|
|
}))}
|
|
onSelect={(id) => setFocusedPhase(id)}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Phases */}
|
|
<section className="container mx-auto max-w-6xl px-4 mt-8">
|
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
{(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";
|
|
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>
|
|
|
|
<section className="container mx-auto max-w-6xl px-4 mt-10">
|
|
<Achievements earnedXp={earnedXp} phaseClaims={phaseClaims} />
|
|
</section>
|
|
|
|
{/* Sneak peeks */}
|
|
<section
|
|
id="sneak-peeks"
|
|
className="container mx-auto max-w-6xl px-4 mt-10"
|
|
>
|
|
<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">Dev Drops</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)}
|
|
disabled={!user}
|
|
className={
|
|
!user ? "cursor-not-allowed opacity-60" : undefined
|
|
}
|
|
title={!user ? "Sign in to unlock" : undefined}
|
|
>
|
|
{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>
|
|
<div className="mt-8">
|
|
<VoteWidget
|
|
options={PEEKS.map((p) => ({ id: p.id, label: p.title }))}
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|