Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/web/src/app/app/blindspot-map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { useBlindspotsQuery } from "@/lib/mock-data/hooks";

export default function BlindspotMapPage() {
const { data: blindspots, isLoading } = useBlindspotsQuery();

if (isLoading || !blindspots) return <LoadingPanel label="Mapping blindspots" />;

return (
<>
<PageHeader eyebrow="blindspot map" title="Weak concepts by evidence" description="A compact view of concepts that need another decode, rebuild, or defend pass." />
<div className="grid gap-4">
{blindspots.map((blindspot) => (
<Card key={blindspot.id}>
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-3">
<CardTitle>{blindspot.concept}</CardTitle>
<Badge variant={blindspot.severity > 70 ? "warning" : "secondary"}>{blindspot.severity}% risk</Badge>
</div>
</CardHeader>
<CardContent>
<Progress value={blindspot.severity} />
<div className="mt-4 grid gap-3 md:grid-cols-2">
<div className="rounded-md border border-border bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">Evidence</p>
<p className="mt-2 text-sm">{blindspot.evidence}</p>
</div>
<div className="rounded-md border border-border bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">Next action</p>
<p className="mt-2 text-sm">{blindspot.nextAction}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</>
);
}
79 changes: 79 additions & 0 deletions apps/web/src/app/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import Link from "next/link";
import { ArrowRight, Clock, Target, Trophy } from "lucide-react";
import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { useDashboardQuery } from "@/lib/mock-data/hooks";
import { IRSRadarChart } from "@/components/features/irs-radar-chart";
import { Leaderboard } from "@/components/features/leaderboard";
import { StreakTracker } from "@/components/features/streak-tracker";

export default function DashboardPage() {
const { data: dashboard, isLoading } = useDashboardQuery();

if (isLoading || !dashboard) return <LoadingPanel />;

const stats = [
{ label: "IRS", value: dashboard.user.irs, copy: "Irreplaceability score", icon: Trophy },
{ label: "Rank", value: `#${dashboard.user.rank}`, copy: "War Room placement", icon: Target },
{ label: "Focus", value: "34m", copy: "Next module estimate", icon: Clock },
];

return (
<>
<PageHeader
eyebrow="dashboard"
title="Training status"
description="Mock data mirrors the future API shape while the backend catches up."
action={<Button asChild><Link href="/app/tracks/frontend-systems/modules/auth-guard-rebuild">Resume module<ArrowRight className="h-4 w-4" /></Link></Button>}
/>
<div className="grid gap-4 md:grid-cols-3">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<Card key={stat.label}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
{stat.label}
<Icon className="h-4 w-4 text-primary" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-semibold">{stat.value}</p>
<p className="mt-1 text-sm text-muted-foreground">{stat.copy}</p>
</CardContent>
</Card>
);
})}
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_360px]">
<Card>
<CardHeader>
<CardTitle>{dashboard.activeTrack.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4 text-sm text-muted-foreground">{dashboard.activeTrack.description}</p>
<Progress value={dashboard.activeTrack.progress} />
<div className="mt-4 grid gap-3 md:grid-cols-2">
{dashboard.activeTrack.modules.map((module) => (
<Link key={module.id} href={`/app/tracks/${dashboard.activeTrack.id}/modules/${module.id}`} className="rounded-md border border-border bg-background/60 p-4 transition hover:border-primary/60">
<p className="font-medium">{module.title}</p>
<p className="mt-2 text-sm text-muted-foreground">{module.summary}</p>
</Link>
))}
</div>
</CardContent>
</Card>
<StreakTracker streak={dashboard.user.streak} />
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
<IRSRadarChart data={dashboard.radarData} />
<Leaderboard entries={dashboard.leaderboard} />
</div>
</>
);
}
5 changes: 5 additions & 0 deletions apps/web/src/app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AppShell } from "@/components/app/app-shell";

export default function ProductLayout({ children }: { children: React.ReactNode }) {
return <AppShell>{children}</AppShell>;
}
5 changes: 5 additions & 0 deletions apps/web/src/app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function AppIndexPage() {
redirect("/app/dashboard");
}
47 changes: 47 additions & 0 deletions apps/web/src/app/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { IRSRadarChart } from "@/components/features/irs-radar-chart";
import { StreakTracker } from "@/components/features/streak-tracker";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useProfileQuery } from "@/lib/mock-data/hooks";

export default function ProfilePage() {
const { data: profile, isLoading } = useProfileQuery();

if (isLoading || !profile) return <LoadingPanel label="Loading profile" />;

return (
<>
<PageHeader eyebrow="profile" title={profile.name} description={profile.role} action={<Badge>IRS {profile.irs}</Badge>} />
<div className="grid gap-4 lg:grid-cols-[1fr_320px]">
<IRSRadarChart data={profile.radarData} />
<StreakTracker streak={profile.streak} />
</div>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Session display</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>Name: {profile.name}</p>
<p>Role: {profile.role}</p>
<p>Completed modules: {profile.completedModules}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recent modules</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{profile.recent.map((item) => (
<div key={item} className="rounded-md border border-border bg-background/60 p-3 text-sm">{item}</div>
))}
</CardContent>
</Card>
</div>
</>
);
}
25 changes: 25 additions & 0 deletions apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { Badge } from "@/components/ui/badge";
import { ModulePlayer } from "@/components/features/module-player";
import { useModuleQuery } from "@/lib/mock-data/hooks";

export default function ModulePage({ params }: { params: { trackId: string; moduleId: string } }) {
const { data, isLoading } = useModuleQuery(params.trackId, params.moduleId);

if (isLoading || !data) return <LoadingPanel label="Loading module player" />;

return (
<>
<PageHeader
eyebrow={data.track.title}
title={data.module.title}
description={data.module.summary}
action={<div className="flex flex-wrap gap-2">{data.module.concepts.map((item) => <Badge key={item} variant="secondary">{item}</Badge>)}</div>}
/>
<ModulePlayer module={data.module} annotations={data.annotations} quiz={data.quiz} diffLines={data.diffLines} />
</>
);
}
52 changes: 52 additions & 0 deletions apps/web/src/app/app/tracks/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { useTracksQuery } from "@/lib/mock-data/hooks";

export default function TracksPage() {
const { data: tracks, isLoading } = useTracksQuery();

if (isLoading || !tracks) return <LoadingPanel />;

return (
<>
<PageHeader eyebrow="tracks" title="Choose a training path" description="Every track is mocked now, but the structures match the planned module flow." />
<div className="grid gap-4 lg:grid-cols-3">
{tracks.map((track) => (
<Card key={track.id}>
<CardHeader>
<div className="flex items-start justify-between gap-3">
<CardTitle>{track.title}</CardTitle>
<Badge variant="outline">{track.difficulty}</Badge>
</div>
</CardHeader>
<CardContent>
<p className="min-h-16 text-sm leading-6 text-muted-foreground">{track.description}</p>
<div className="my-4">
<div className="mb-2 flex justify-between text-xs text-muted-foreground">
<span>Progress</span>
<span>{track.progress}%</span>
</div>
<Progress value={track.progress} />
</div>
<div className="space-y-2">
{track.modules.map((module) => (
<Link key={module.id} href={`/app/tracks/${track.id}/modules/${module.id}`} className="flex items-center justify-between rounded-md border border-border bg-background/60 p-3 text-sm transition hover:border-primary/60">
<span>{module.title}</span>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</Link>
))}
</div>
</CardContent>
</Card>
))}
</div>
</>
);
}
25 changes: 25 additions & 0 deletions apps/web/src/app/app/war-room/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { PageHeader } from "@/components/app/page-header";
import { LoadingPanel } from "@/components/app/loading-panel";
import { Badge } from "@/components/ui/badge";
import { WarRoomLive } from "@/components/features/war-room-live";
import { useWarRoomQuery } from "@/lib/mock-data/hooks";

export default function WarRoomPage() {
const { data, isLoading } = useWarRoomQuery();

if (isLoading || !data) return <LoadingPanel label="Joining War Room" />;

return (
<>
<PageHeader
eyebrow="war room"
title={data.room.name}
description="Socket.io client wiring is present with a mock live feed so the room works without backend events."
action={<Badge variant="success">{data.room.members} members live</Badge>}
/>
<WarRoomLive messages={data.messages} leaderboard={data.leaderboard} />
</>
);
}
46 changes: 46 additions & 0 deletions apps/web/src/app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import Link from "next/link";
import { Github, Mail } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";

export default function SignInPage() {
const { signIn } = useAuthStore();

return (
<main className="surface-grid flex min-h-screen items-center justify-center p-4">
<Card className="w-full max-w-md bg-card/95 backdrop-blur">
<CardHeader>
<Link href="/" className="mb-6 flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-primary/15 font-mono text-sm font-semibold text-primary">UV</span>
<span className="font-semibold">UnVibe</span>
</Link>
<CardTitle className="text-2xl">Sign in</CardTitle>
<p className="text-sm text-muted-foreground">Use OAuth later, or enter the mock workspace now.</p>
</CardHeader>
<CardContent className="space-y-3">
<Button className="w-full" variant="outline" onClick={signIn}>
<Github className="h-4 w-4" />
Continue with GitHub
</Button>
<Button className="w-full" variant="outline" onClick={signIn}>
<Mail className="h-4 w-4" />
Continue with Google
</Button>
<div className="grid gap-2 pt-3">
<Input placeholder="email@company.com" type="email" />
<Button asChild onClick={signIn}>
<Link href="/app/dashboard">Enter mock workspace</Link>
</Button>
</div>
<p className="pt-2 text-center text-sm text-muted-foreground">
New here? <Link className="text-primary" href="/auth/signup">Create an account</Link>
</p>
</CardContent>
</Card>
</main>
);
}
47 changes: 47 additions & 0 deletions apps/web/src/app/auth/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import Link from "next/link";
import { Github, Mail } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";

export default function SignUpPage() {
const { signIn } = useAuthStore();

return (
<main className="surface-grid flex min-h-screen items-center justify-center p-4">
<Card className="w-full max-w-md bg-card/95 backdrop-blur">
<CardHeader>
<Link href="/" className="mb-6 flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-primary/15 font-mono text-sm font-semibold text-primary">UV</span>
<span className="font-semibold">UnVibe</span>
</Link>
<CardTitle className="text-2xl">Create account</CardTitle>
<p className="text-sm text-muted-foreground">Mock onboarding keeps frontend work unblocked.</p>
</CardHeader>
<CardContent className="space-y-3">
<Button className="w-full" variant="outline" onClick={signIn}>
<Github className="h-4 w-4" />
Sign up with GitHub
</Button>
<Button className="w-full" variant="outline" onClick={signIn}>
<Mail className="h-4 w-4" />
Sign up with Google
</Button>
<div className="grid gap-2 pt-3">
<Input placeholder="Your name" />
<Input placeholder="email@company.com" type="email" />
<Button asChild onClick={signIn}>
<Link href="/app/dashboard">Create mock account</Link>
</Button>
</div>
<p className="pt-2 text-center text-sm text-muted-foreground">
Already training? <Link className="text-primary" href="/auth/signin">Sign in</Link>
</p>
</CardContent>
</Card>
</main>
);
}
Loading