Prettier format pending files
This commit is contained in:
parent
c87f582775
commit
0d41408ce0
10 changed files with 616 additions and 120 deletions
|
|
@ -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 — 0–2 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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue