From 168c2933548200355869fa8f1666df69b1a0dee1 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Sun, 28 Jun 2026 12:41:06 +0530
Subject: [PATCH 1/5] feat(web): add command center UI foundation
---
apps/web/src/app/globals.css | 81 +++++++++++--------
apps/web/src/app/layout.tsx | 2 +-
apps/web/src/components/app/loading-panel.tsx | 12 +++
apps/web/src/components/app/page-header.tsx | 14 ++++
.../src/components/app/theme-controller.tsx | 20 +++++
apps/web/src/components/ui/badge.tsx | 25 ++++++
apps/web/src/components/ui/button.tsx | 46 +++++++++++
apps/web/src/components/ui/card.tsx | 22 +++++
apps/web/src/components/ui/input.tsx | 14 ++++
apps/web/src/components/ui/progress.tsx | 10 +++
apps/web/src/components/ui/textarea.tsx | 14 ++++
11 files changed, 227 insertions(+), 33 deletions(-)
create mode 100644 apps/web/src/components/app/loading-panel.tsx
create mode 100644 apps/web/src/components/app/page-header.tsx
create mode 100644 apps/web/src/components/app/theme-controller.tsx
create mode 100644 apps/web/src/components/ui/badge.tsx
create mode 100644 apps/web/src/components/ui/button.tsx
create mode 100644 apps/web/src/components/ui/card.tsx
create mode 100644 apps/web/src/components/ui/input.tsx
create mode 100644 apps/web/src/components/ui/progress.tsx
create mode 100644 apps/web/src/components/ui/textarea.tsx
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 8abdb15..46aad00 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -4,65 +4,65 @@
@layer base {
:root {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
+ --background: 210 25% 98%;
+ --foreground: 218 33% 10%;
--card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
+ --card-foreground: 218 33% 10%;
--popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
+ --popover-foreground: 218 33% 10%;
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
+ --primary: 188 91% 35%;
+ --primary-foreground: 190 90% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 214 32% 91%;
+ --secondary-foreground: 218 33% 12%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
+ --muted: 215 28% 92%;
+ --muted-foreground: 216 14% 38%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
+ --accent: 41 96% 56%;
+ --accent-foreground: 32 95% 12%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 222.2 84% 4.9%;
+ --border: 214 20% 84%;
+ --input: 214 20% 84%;
+ --ring: 188 91% 35%;
--radius: 0.5rem;
}
.dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
+ --background: 220 24% 6%;
+ --foreground: 213 31% 91%;
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
+ --card: 220 20% 10%;
+ --card-foreground: 213 31% 91%;
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
+ --popover: 220 20% 10%;
+ --popover-foreground: 213 31% 91%;
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
+ --primary: 187 85% 52%;
+ --primary-foreground: 220 24% 6%;
- --secondary: 217.2 32.6% 17.5%;
- --secondary-foreground: 210 40% 98%;
+ --secondary: 218 18% 16%;
+ --secondary-foreground: 213 31% 91%;
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
+ --muted: 218 18% 14%;
+ --muted-foreground: 215 18% 63%;
- --accent: 217.2 32.6% 17.5%;
- --accent-foreground: 210 40% 98%;
+ --accent: 42 94% 58%;
+ --accent-foreground: 220 24% 6%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
+ --border: 218 16% 21%;
+ --input: 218 16% 21%;
+ --ring: 187 85% 52%;
}
}
@@ -73,4 +73,21 @@
body {
@apply bg-background text-foreground;
}
+
+ ::selection {
+ @apply bg-primary/30 text-foreground;
+ }
+}
+
+@layer utilities {
+ .surface-grid {
+ background-image:
+ linear-gradient(hsl(var(--border) / 0.3) 1px, transparent 1px),
+ linear-gradient(90deg, hsl(var(--border) / 0.3) 1px, transparent 1px);
+ background-size: 28px 28px;
+ }
+
+ .text-balance {
+ text-wrap: balance;
+ }
}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index d75a899..35a82c3 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -25,7 +25,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
diff --git a/apps/web/src/components/app/loading-panel.tsx b/apps/web/src/components/app/loading-panel.tsx
new file mode 100644
index 0000000..be2187a
--- /dev/null
+++ b/apps/web/src/components/app/loading-panel.tsx
@@ -0,0 +1,12 @@
+import { Card, CardContent } from "@/components/ui/card";
+
+export function LoadingPanel({ label = "Loading mock data" }: { label?: string }) {
+ return (
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/apps/web/src/components/app/page-header.tsx b/apps/web/src/components/app/page-header.tsx
new file mode 100644
index 0000000..86bb1be
--- /dev/null
+++ b/apps/web/src/components/app/page-header.tsx
@@ -0,0 +1,14 @@
+import { Badge } from "@/components/ui/badge";
+
+export function PageHeader({ eyebrow, title, description, action }: { eyebrow?: string; title: string; description?: string; action?: React.ReactNode }) {
+ return (
+
+
+ {eyebrow ?
{eyebrow} : null}
+
{title}
+ {description ?
{description}
: null}
+
+ {action}
+
+ );
+}
diff --git a/apps/web/src/components/app/theme-controller.tsx b/apps/web/src/components/app/theme-controller.tsx
new file mode 100644
index 0000000..edc6b65
--- /dev/null
+++ b/apps/web/src/components/app/theme-controller.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { useEffect } from "react";
+import { Moon, Sun } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useUIStore } from "@/stores/ui-store";
+
+export function ThemeController() {
+ const { darkMode, toggleDarkMode } = useUIStore();
+
+ useEffect(() => {
+ document.documentElement.classList.toggle("dark", darkMode);
+ }, [darkMode]);
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
new file mode 100644
index 0000000..e6f39a5
--- /dev/null
+++ b/apps/web/src/components/ui/badge.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+type BadgeVariant = "default" | "secondary" | "outline" | "success" | "warning";
+
+const variants: Record = {
+ default: "border-transparent bg-primary text-primary-foreground",
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
+ outline: "border-border text-foreground",
+ success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400",
+ warning: "border-amber-500/30 bg-amber-500/10 text-amber-300",
+};
+
+export function Badge({ className, variant = "default", ...props }: React.HTMLAttributes & { variant?: BadgeVariant }) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
new file mode 100644
index 0000000..e8edad6
--- /dev/null
+++ b/apps/web/src/components/ui/button.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cn } from "@/lib/utils";
+
+type ButtonVariant = "default" | "secondary" | "ghost" | "outline" | "destructive";
+type ButtonSize = "default" | "sm" | "lg" | "icon";
+
+const variants: Record = {
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-muted hover:text-foreground",
+ outline: "border border-border bg-background hover:bg-muted",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+};
+
+const sizes: Record = {
+ default: "h-10 px-4 py-2",
+ sm: "h-8 px-3 text-xs",
+ lg: "h-11 px-5",
+ icon: "h-10 w-10",
+};
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ asChild?: boolean;
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+}
+
+export const Button = React.forwardRef(
+ ({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ }
+);
+Button.displayName = "Button";
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
new file mode 100644
index 0000000..d785a3c
--- /dev/null
+++ b/apps/web/src/components/ui/card.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export function Card({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardHeader({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardTitle({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardDescription({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
+export function CardContent({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
new file mode 100644
index 0000000..7a492f4
--- /dev/null
+++ b/apps/web/src/components/ui/input.tsx
@@ -0,0 +1,14 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const Input = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+Input.displayName = "Input";
diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx
new file mode 100644
index 0000000..2ea6d9f
--- /dev/null
+++ b/apps/web/src/components/ui/progress.tsx
@@ -0,0 +1,10 @@
+import { cn } from "@/lib/utils";
+
+export function Progress({ value, className }: { value: number; className?: string }) {
+ const width = Math.max(0, Math.min(100, value));
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..ac13df7
--- /dev/null
+++ b/apps/web/src/components/ui/textarea.tsx
@@ -0,0 +1,14 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const Textarea = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+Textarea.displayName = "Textarea";
From 99c6ae1caf99447c8cd6261aa0e38082b9bdc387 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Sun, 28 Jun 2026 12:41:22 +0530
Subject: [PATCH 2/5] feat(web): add mock data and client state
---
apps/web/src/lib/mock-data/api.ts | 58 +++++++++++
apps/web/src/lib/mock-data/data.ts | 150 ++++++++++++++++++++++++++++
apps/web/src/lib/mock-data/hooks.ts | 28 ++++++
apps/web/src/lib/mock-data/types.ts | 69 +++++++++++++
apps/web/src/lib/socket/client.ts | 16 +++
apps/web/src/lib/trpc/client.ts | 13 +++
apps/web/src/stores/auth-store.ts | 26 +++++
apps/web/src/stores/editor-store.ts | 26 +++++
apps/web/src/stores/ui-store.ts | 17 ++++
9 files changed, 403 insertions(+)
create mode 100644 apps/web/src/lib/mock-data/api.ts
create mode 100644 apps/web/src/lib/mock-data/data.ts
create mode 100644 apps/web/src/lib/mock-data/hooks.ts
create mode 100644 apps/web/src/lib/mock-data/types.ts
create mode 100644 apps/web/src/lib/socket/client.ts
create mode 100644 apps/web/src/lib/trpc/client.ts
create mode 100644 apps/web/src/stores/auth-store.ts
create mode 100644 apps/web/src/stores/editor-store.ts
create mode 100644 apps/web/src/stores/ui-store.ts
diff --git a/apps/web/src/lib/mock-data/api.ts b/apps/web/src/lib/mock-data/api.ts
new file mode 100644
index 0000000..9d41964
--- /dev/null
+++ b/apps/web/src/lib/mock-data/api.ts
@@ -0,0 +1,58 @@
+import { annotations, blindspots, diffLines, leaderboard, quiz, radarData, tracks, warRoomMessages } from "./data";
+
+const wait = (ms = 240) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export async function getDashboard() {
+ await wait();
+ return {
+ user: { name: "Sourabh", irs: 82, streak: 7, rank: 3 },
+ activeTrack: tracks[0],
+ radarData,
+ leaderboard,
+ blindspots,
+ };
+}
+
+export async function getTracks() {
+ await wait();
+ return tracks;
+}
+
+export async function getModule(trackId: string, moduleId: string) {
+ await wait();
+ const track = tracks.find((item) => item.id === trackId) ?? tracks[0];
+ return {
+ track,
+ module: track.modules.find((item) => item.id === moduleId) ?? track.modules[0],
+ annotations,
+ quiz,
+ diffLines,
+ };
+}
+
+export async function getWarRoom() {
+ await wait();
+ return {
+ room: { id: "room-1", name: "Frontend Systems live defend", members: 18, status: "live" },
+ messages: warRoomMessages,
+ leaderboard,
+ };
+}
+
+export async function getProfile() {
+ await wait();
+ return {
+ name: "Sourabh Patne",
+ role: "Frontend builder",
+ irs: 82,
+ streak: 7,
+ completedModules: 14,
+ radarData,
+ recent: ["Auth guard rebuild", "Query cache policy", "Diff score contract"],
+ };
+}
+
+export async function getBlindspots() {
+ await wait();
+ return blindspots;
+}
diff --git a/apps/web/src/lib/mock-data/data.ts b/apps/web/src/lib/mock-data/data.ts
new file mode 100644
index 0000000..5249b0f
--- /dev/null
+++ b/apps/web/src/lib/mock-data/data.ts
@@ -0,0 +1,150 @@
+import type { Annotation, Blindspot, DiffLine, LeaderboardEntry, MockTrack, QuizQuestion, WarRoomMessage } from "./types";
+
+const sourceCode = `export function createGuard(session) {
+ if (!session?.user) {
+ return { allowed: false, reason: "Missing user" };
+ }
+
+ const roles = session.user.roles ?? [];
+ return {
+ allowed: roles.includes("builder"),
+ reason: roles.includes("builder") ? "Ready" : "Builder role required",
+ };
+}`;
+
+export const tracks: MockTrack[] = [
+ {
+ id: "frontend-systems",
+ title: "Frontend Systems",
+ description: "State, data fetching, auth surfaces, and editor-heavy product screens.",
+ difficulty: "Intermediate",
+ progress: 68,
+ modules: [
+ {
+ id: "auth-guard-rebuild",
+ trackId: "frontend-systems",
+ title: "Auth guard rebuild",
+ summary: "Decode a session guard and rebuild its branching logic from memory.",
+ order: 1,
+ estimatedMinutes: 34,
+ sourceCode,
+ starterCode: `export function createGuard(session) {\n // Rebuild the guard from memory.\n}`,
+ language: "typescript",
+ concepts: ["auth", "branching", "defensive checks"],
+ },
+ {
+ id: "query-cache",
+ trackId: "frontend-systems",
+ title: "Query cache policy",
+ summary: "Reason about stale time, optimistic data, and recovery states.",
+ order: 2,
+ estimatedMinutes: 28,
+ sourceCode,
+ starterCode: sourceCode,
+ language: "typescript",
+ concepts: ["react-query", "cache", "latency"],
+ },
+ ],
+ },
+ {
+ id: "ai-workflows",
+ title: "AI Workflows",
+ description: "Prompt contracts, diff scoring, quiz generation, and defend sessions.",
+ difficulty: "Advanced",
+ progress: 42,
+ modules: [
+ {
+ id: "diff-score-contract",
+ trackId: "ai-workflows",
+ title: "Diff score contract",
+ summary: "Compare code intent instead of matching text line by line.",
+ order: 1,
+ estimatedMinutes: 41,
+ sourceCode,
+ starterCode: sourceCode,
+ language: "typescript",
+ concepts: ["diff", "scoring", "contracts"],
+ },
+ ],
+ },
+ {
+ id: "backend-foundations",
+ title: "Backend Foundations",
+ description: "tRPC procedures, Prisma access patterns, queue jobs, and socket events.",
+ difficulty: "Foundation",
+ progress: 24,
+ modules: [
+ {
+ id: "trpc-health",
+ trackId: "backend-foundations",
+ title: "tRPC health procedure",
+ summary: "Trace a thin procedure from client call to Express middleware.",
+ order: 1,
+ estimatedMinutes: 22,
+ sourceCode,
+ starterCode: sourceCode,
+ language: "typescript",
+ concepts: ["trpc", "express", "routing"],
+ },
+ ],
+ },
+];
+
+export const annotations: Annotation[] = [
+ { id: "a1", line: 2, tag: "guard", note: "Reject missing sessions before reading nested fields." },
+ { id: "a2", line: 6, tag: "roles", note: "Default roles to an empty array so includes is safe." },
+];
+
+export const quiz: QuizQuestion[] = [
+ {
+ id: "q1",
+ prompt: "Why does the guard use optional chaining on session?",
+ options: ["To make the code shorter", "To avoid reading user from null or undefined", "To memoize the session", "To force OAuth refresh"],
+ answerIndex: 1,
+ explanation: "The first branch must be safe even when there is no session object.",
+ },
+ {
+ id: "q2",
+ prompt: "What behavior should remain stable in the rebuild?",
+ options: ["Only exact whitespace", "The builder role check and failure reason", "The import order", "The variable names only"],
+ answerIndex: 1,
+ explanation: "The important behavior is access control and clear feedback, not identical formatting.",
+ },
+];
+
+export const diffLines: DiffLine[] = [
+ { id: "d1", type: "same", left: "export function createGuard(session) {", right: "export function createGuard(session) {" },
+ { id: "d2", type: "same", left: " if (!session?.user) {", right: " if (!session?.user) {" },
+ { id: "d3", type: "remove", left: " return { allowed: false, reason: \"Missing user\" };" },
+ { id: "d4", type: "add", right: " return { allowed: false, reason: \"No user\" };" },
+ { id: "d5", type: "same", left: " const roles = session.user.roles ?? [];", right: " const roles = session.user.roles ?? [];" },
+ { id: "d6", type: "same", left: "}", right: "}" },
+];
+
+export const leaderboard: LeaderboardEntry[] = [
+ { id: "u1", name: "Aarav", score: 91, streak: 14, track: "Frontend Systems" },
+ { id: "u2", name: "Mira", score: 87, streak: 11, track: "AI Workflows" },
+ { id: "u3", name: "You", score: 82, streak: 7, track: "Frontend Systems" },
+ { id: "u4", name: "Dev", score: 78, streak: 5, track: "Backend Foundations" },
+];
+
+export const warRoomMessages: WarRoomMessage[] = [
+ { id: "m1", author: "System", body: "Room opened for Auth guard rebuild.", timestamp: "11:42", kind: "system" },
+ { id: "m2", author: "Mira", body: "Watch the default roles branch. It is easy to miss.", timestamp: "11:44", kind: "chat" },
+ { id: "m3", author: "Defend Bot", body: "Explain why a missing role should return a reason string.", timestamp: "11:45", kind: "defend" },
+];
+
+export const blindspots: Blindspot[] = [
+ { id: "b1", concept: "Null-safe access", severity: 78, evidence: "Missed 2 optional chaining branches", nextAction: "Replay auth guard decode" },
+ { id: "b2", concept: "Cache invalidation", severity: 62, evidence: "Submitted stale query policy twice", nextAction: "Rebuild query cache module" },
+ { id: "b3", concept: "Role semantics", severity: 44, evidence: "Weak defend answer on authorization reasons", nextAction: "Answer 3 defend prompts" },
+];
+
+export const radarData = [
+ { subject: "Read", score: 86 },
+ { subject: "Rebuild", score: 74 },
+ { subject: "Defend", score: 68 },
+ { subject: "Speed", score: 71 },
+ { subject: "Recall", score: 82 },
+ { subject: "Review", score: 64 },
+];
diff --git a/apps/web/src/lib/mock-data/hooks.ts b/apps/web/src/lib/mock-data/hooks.ts
new file mode 100644
index 0000000..1bd9547
--- /dev/null
+++ b/apps/web/src/lib/mock-data/hooks.ts
@@ -0,0 +1,28 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { getBlindspots, getDashboard, getModule, getProfile, getTracks, getWarRoom } from "./api";
+
+export function useDashboardQuery() {
+ return useQuery({ queryKey: ["dashboard"], queryFn: getDashboard });
+}
+
+export function useTracksQuery() {
+ return useQuery({ queryKey: ["tracks"], queryFn: getTracks });
+}
+
+export function useModuleQuery(trackId: string, moduleId: string) {
+ return useQuery({ queryKey: ["module", trackId, moduleId], queryFn: () => getModule(trackId, moduleId) });
+}
+
+export function useWarRoomQuery() {
+ return useQuery({ queryKey: ["war-room"], queryFn: getWarRoom });
+}
+
+export function useProfileQuery() {
+ return useQuery({ queryKey: ["profile"], queryFn: getProfile });
+}
+
+export function useBlindspotsQuery() {
+ return useQuery({ queryKey: ["blindspots"], queryFn: getBlindspots });
+}
diff --git a/apps/web/src/lib/mock-data/types.ts b/apps/web/src/lib/mock-data/types.ts
new file mode 100644
index 0000000..513bfe2
--- /dev/null
+++ b/apps/web/src/lib/mock-data/types.ts
@@ -0,0 +1,69 @@
+export type ModulePhase = "decode" | "rebuild" | "defend";
+
+export interface MockTrack {
+ id: string;
+ title: string;
+ description: string;
+ difficulty: "Foundation" | "Intermediate" | "Advanced";
+ progress: number;
+ modules: MockModule[];
+}
+
+export interface MockModule {
+ id: string;
+ trackId: string;
+ title: string;
+ summary: string;
+ order: number;
+ estimatedMinutes: number;
+ sourceCode: string;
+ starterCode: string;
+ language: string;
+ concepts: string[];
+}
+
+export interface Annotation {
+ id: string;
+ line: number;
+ note: string;
+ tag: string;
+}
+
+export interface QuizQuestion {
+ id: string;
+ prompt: string;
+ options: string[];
+ answerIndex: number;
+ explanation: string;
+}
+
+export interface DiffLine {
+ id: string;
+ type: "same" | "add" | "remove";
+ left?: string;
+ right?: string;
+}
+
+export interface LeaderboardEntry {
+ id: string;
+ name: string;
+ score: number;
+ streak: number;
+ track: string;
+}
+
+export interface WarRoomMessage {
+ id: string;
+ author: string;
+ body: string;
+ timestamp: string;
+ kind: "chat" | "system" | "defend";
+}
+
+export interface Blindspot {
+ id: string;
+ concept: string;
+ severity: number;
+ evidence: string;
+ nextAction: string;
+}
diff --git a/apps/web/src/lib/socket/client.ts b/apps/web/src/lib/socket/client.ts
new file mode 100644
index 0000000..5e89f60
--- /dev/null
+++ b/apps/web/src/lib/socket/client.ts
@@ -0,0 +1,16 @@
+"use client";
+
+import { io, type Socket } from "socket.io-client";
+
+let socket: Socket | null = null;
+
+export function getSocket() {
+ if (!socket) {
+ socket = io(process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000", {
+ autoConnect: false,
+ transports: ["websocket"],
+ });
+ }
+
+ return socket;
+}
diff --git a/apps/web/src/lib/trpc/client.ts b/apps/web/src/lib/trpc/client.ts
new file mode 100644
index 0000000..e50447a
--- /dev/null
+++ b/apps/web/src/lib/trpc/client.ts
@@ -0,0 +1,13 @@
+export const trpcEndpoint = `${process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"}/trpc`;
+
+export async function callTrpcHealth() {
+ const response = await fetch(`${trpcEndpoint}/health`, {
+ method: "GET",
+ });
+
+ if (!response.ok) {
+ throw new Error("tRPC health check failed");
+ }
+
+ return response.json();
+}
diff --git a/apps/web/src/stores/auth-store.ts b/apps/web/src/stores/auth-store.ts
new file mode 100644
index 0000000..acfc349
--- /dev/null
+++ b/apps/web/src/stores/auth-store.ts
@@ -0,0 +1,26 @@
+"use client";
+
+import { create } from "zustand";
+
+interface MockUser {
+ name: string;
+ email: string;
+ image?: string;
+}
+
+interface AuthStore {
+ user: MockUser | null;
+ signIn: () => void;
+ signOut: () => void;
+}
+
+const defaultUser: MockUser = {
+ name: "Sourabh Patne",
+ email: "sourabh@unvibe.dev",
+};
+
+export const useAuthStore = create((set) => ({
+ user: defaultUser,
+ signIn: () => set({ user: defaultUser }),
+ signOut: () => set({ user: null }),
+}));
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts
new file mode 100644
index 0000000..3fec86c
--- /dev/null
+++ b/apps/web/src/stores/editor-store.ts
@@ -0,0 +1,26 @@
+"use client";
+
+import { create } from "zustand";
+import type { ModulePhase } from "@/lib/mock-data/types";
+
+interface EditorStore {
+ phase: ModulePhase;
+ code: string;
+ language: string;
+ isDirty: boolean;
+ setPhase: (phase: ModulePhase) => void;
+ setCode: (code: string) => void;
+ resetCode: (code: string) => void;
+ setLanguage: (language: string) => void;
+}
+
+export const useEditorStore = create((set) => ({
+ phase: "decode",
+ code: "",
+ language: "typescript",
+ isDirty: false,
+ setPhase: (phase) => set({ phase }),
+ setCode: (code) => set({ code, isDirty: true }),
+ resetCode: (code) => set({ code, isDirty: false }),
+ setLanguage: (language) => set({ language }),
+}));
diff --git a/apps/web/src/stores/ui-store.ts b/apps/web/src/stores/ui-store.ts
new file mode 100644
index 0000000..fa7833d
--- /dev/null
+++ b/apps/web/src/stores/ui-store.ts
@@ -0,0 +1,17 @@
+"use client";
+
+import { create } from "zustand";
+
+interface UIStore {
+ darkMode: boolean;
+ sidebarOpen: boolean;
+ toggleDarkMode: () => void;
+ toggleSidebar: () => void;
+}
+
+export const useUIStore = create((set) => ({
+ darkMode: true,
+ sidebarOpen: false,
+ toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
+}));
From 87baf3e32ac13eae496fd7a57c032441fb3c3a29 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Sun, 28 Jun 2026 12:41:56 +0530
Subject: [PATCH 3/5] feat(web): build interactive learning components
---
apps/web/src/components/app/app-shell.tsx | 96 +++++++++++++
.../components/features/annotation-editor.tsx | 39 ++++++
.../src/components/features/code-editor.tsx | 36 +++++
.../components/features/code-submission.tsx | 33 +++++
.../src/components/features/diff-viewer.tsx | 21 +++
.../components/features/irs-radar-chart.tsx | 23 ++++
.../src/components/features/leaderboard.tsx | 30 +++++
.../src/components/features/module-player.tsx | 126 ++++++++++++++++++
apps/web/src/components/features/quiz-ui.tsx | 46 +++++++
.../components/features/streak-tracker.tsx | 21 +++
.../src/components/features/war-room-live.tsx | 72 ++++++++++
11 files changed, 543 insertions(+)
create mode 100644 apps/web/src/components/app/app-shell.tsx
create mode 100644 apps/web/src/components/features/annotation-editor.tsx
create mode 100644 apps/web/src/components/features/code-editor.tsx
create mode 100644 apps/web/src/components/features/code-submission.tsx
create mode 100644 apps/web/src/components/features/diff-viewer.tsx
create mode 100644 apps/web/src/components/features/irs-radar-chart.tsx
create mode 100644 apps/web/src/components/features/leaderboard.tsx
create mode 100644 apps/web/src/components/features/module-player.tsx
create mode 100644 apps/web/src/components/features/quiz-ui.tsx
create mode 100644 apps/web/src/components/features/streak-tracker.tsx
create mode 100644 apps/web/src/components/features/war-room-live.tsx
diff --git a/apps/web/src/components/app/app-shell.tsx b/apps/web/src/components/app/app-shell.tsx
new file mode 100644
index 0000000..0019165
--- /dev/null
+++ b/apps/web/src/components/app/app-shell.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { BarChart3, LayoutDashboard, Map, RadioTower, Route, UserRound } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { ThemeController } from "./theme-controller";
+import { useAuthStore } from "@/stores/auth-store";
+import { Button } from "@/components/ui/button";
+
+const nav = [
+ { href: "/app/dashboard", label: "Dashboard", icon: LayoutDashboard },
+ { href: "/app/tracks", label: "Tracks", icon: Route },
+ { href: "/app/war-room", label: "War Room", icon: RadioTower },
+ { href: "/app/blindspot-map", label: "Blindspots", icon: Map },
+ { href: "/app/profile", label: "Profile", icon: UserRound },
+];
+
+export function AppShell({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const { user, signOut } = useAuthStore();
+
+ return (
+
+
+
+
+
+
+ UnVibe
+
+
+
+
+
+
{user?.name ?? "Guest"}
+
{user?.email ?? "mock session"}
+
+
+
+
+
{children}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/features/annotation-editor.tsx b/apps/web/src/components/features/annotation-editor.tsx
new file mode 100644
index 0000000..a91f9d2
--- /dev/null
+++ b/apps/web/src/components/features/annotation-editor.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { useState } from "react";
+import type { Annotation } from "@/lib/mock-data/types";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+
+export function AnnotationEditor({ annotations: initial }: { annotations: Annotation[] }) {
+ const [annotations, setAnnotations] = useState(initial);
+ const [note, setNote] = useState("");
+
+ return (
+
+
+
+ {annotations.map((annotation) => (
+
+
+ line {annotation.line}
+ {annotation.tag}
+
+
{annotation.note}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/components/features/code-editor.tsx b/apps/web/src/components/features/code-editor.tsx
new file mode 100644
index 0000000..a332721
--- /dev/null
+++ b/apps/web/src/components/features/code-editor.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import Editor from "@monaco-editor/react";
+import { RotateCcw } from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+export function CodeEditor({ code, language, onChange, onReset, readOnly = false }: { code: string; language: string; onChange: (value: string) => void; onReset?: () => void; readOnly?: boolean }) {
+ return (
+
+
+ {language}
+ {onReset ? (
+
+ ) : null}
+
+
onChange(value ?? "")}
+ options={{
+ readOnly,
+ minimap: { enabled: false },
+ fontSize: 13,
+ lineNumbersMinChars: 3,
+ scrollBeyondLastLine: false,
+ wordWrap: "on",
+ }}
+ />
+
+ );
+}
diff --git a/apps/web/src/components/features/code-submission.tsx b/apps/web/src/components/features/code-submission.tsx
new file mode 100644
index 0000000..674be56
--- /dev/null
+++ b/apps/web/src/components/features/code-submission.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { useState } from "react";
+import { Send } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+
+export function CodeSubmission({ disabled }: { disabled?: boolean }) {
+ const [status, setStatus] = useState<"idle" | "running" | "passed">("idle");
+
+ return (
+
+
+
+
Submission
+
Mock diff scoring keeps the flow local.
+
+
{status}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/features/diff-viewer.tsx b/apps/web/src/components/features/diff-viewer.tsx
new file mode 100644
index 0000000..6897230
--- /dev/null
+++ b/apps/web/src/components/features/diff-viewer.tsx
@@ -0,0 +1,21 @@
+import type { DiffLine } from "@/lib/mock-data/types";
+import { cn } from "@/lib/utils";
+
+export function DiffViewer({ lines }: { lines: DiffLine[] }) {
+ return (
+
+
+ source
+ rebuild
+
+
+ {lines.map((line) => (
+
+
{line.left ?? ""}
+
{line.right ?? ""}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/features/irs-radar-chart.tsx b/apps/web/src/components/features/irs-radar-chart.tsx
new file mode 100644
index 0000000..f812314
--- /dev/null
+++ b/apps/web/src/components/features/irs-radar-chart.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { PolarAngleAxis, PolarGrid, Radar, RadarChart, ResponsiveContainer } from "recharts";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export function IRSRadarChart({ data }: { data: Array<{ subject: string; score: number }> }) {
+ return (
+
+
+ IRS radar
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/features/leaderboard.tsx b/apps/web/src/components/features/leaderboard.tsx
new file mode 100644
index 0000000..11ce94d
--- /dev/null
+++ b/apps/web/src/components/features/leaderboard.tsx
@@ -0,0 +1,30 @@
+import type { LeaderboardEntry } from "@/lib/mock-data/types";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+export function Leaderboard({ entries }: { entries: LeaderboardEntry[] }) {
+ return (
+
+
+ Leaderboard
+
+
+ {entries.map((entry, index) => (
+
+
+
#{index + 1}
+
+
{entry.name}
+
{entry.track}
+
+
+
+ {entry.score}
+ {entry.streak}d
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/features/module-player.tsx b/apps/web/src/components/features/module-player.tsx
new file mode 100644
index 0000000..4df69f9
--- /dev/null
+++ b/apps/web/src/components/features/module-player.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import { BookOpenCheck, Brain, MessageSquareQuote } from "lucide-react";
+import type { Annotation, DiffLine, MockModule, QuizQuestion } from "@/lib/mock-data/types";
+import { useEditorStore } from "@/stores/editor-store";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { AnnotationEditor } from "./annotation-editor";
+import { CodeEditor } from "./code-editor";
+import { CodeSubmission } from "./code-submission";
+import { DiffViewer } from "./diff-viewer";
+import { QuizUI } from "./quiz-ui";
+
+const phases = [
+ { id: "decode", label: "Decode", icon: BookOpenCheck },
+ { id: "rebuild", label: "Rebuild", icon: Brain },
+ { id: "defend", label: "Defend", icon: MessageSquareQuote },
+] as const;
+
+export function ModulePlayer({ module, annotations, quiz, diffLines }: { module: MockModule; annotations: Annotation[]; quiz: QuizQuestion[]; diffLines: DiffLine[] }) {
+ const { phase, setPhase, code, setCode, resetCode, language } = useEditorStore();
+ const [defendAnswers, setDefendAnswers] = useState(["I would preserve the early return so later branches can assume a user exists."]);
+
+ useEffect(() => {
+ resetCode(module.starterCode);
+ }, [module.id, module.starterCode, resetCode]);
+
+ const progress = useMemo(() => (phase === "decode" ? 33 : phase === "rebuild" ? 66 : 100), [phase]);
+
+ return (
+
+
+
+
+
+
+
{module.title}
+
{module.summary}
+
+
{module.estimatedMinutes} min
+
+
+
+
+ {phases.map((item) => {
+ const Icon = item.icon;
+ const active = item.id === phase;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {phase === "decode" ? (
+ undefined} readOnly />
+ ) : (
+ resetCode(module.starterCode)} />
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/features/quiz-ui.tsx b/apps/web/src/components/features/quiz-ui.tsx
new file mode 100644
index 0000000..3d5cd71
--- /dev/null
+++ b/apps/web/src/components/features/quiz-ui.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useState } from "react";
+import type { QuizQuestion } from "@/lib/mock-data/types";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+export function QuizUI({ questions }: { questions: QuizQuestion[] }) {
+ const [index, setIndex] = useState(0);
+ const [selected, setSelected] = useState(null);
+ const question = questions[index];
+ const complete = index >= questions.length;
+
+ if (complete) {
+ return Quiz complete. Rebuild is unlocked.
;
+ }
+
+ return (
+
+
{question.prompt}
+
+ {question.options.map((option, optionIndex) => {
+ const picked = selected === optionIndex;
+ const correct = selected !== null && optionIndex === question.answerIndex;
+ return (
+
+ );
+ })}
+
+ {selected !== null ?
{question.explanation}
: null}
+
+
+ );
+}
diff --git a/apps/web/src/components/features/streak-tracker.tsx b/apps/web/src/components/features/streak-tracker.tsx
new file mode 100644
index 0000000..64c086d
--- /dev/null
+++ b/apps/web/src/components/features/streak-tracker.tsx
@@ -0,0 +1,21 @@
+import { Flame } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export function StreakTracker({ streak }: { streak: number }) {
+ return (
+
+
+ Streak
+
+
+
+
+
+
{streak}
+
days in training
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/features/war-room-live.tsx b/apps/web/src/components/features/war-room-live.tsx
new file mode 100644
index 0000000..ceeb770
--- /dev/null
+++ b/apps/web/src/components/features/war-room-live.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import type { LeaderboardEntry, WarRoomMessage } from "@/lib/mock-data/types";
+import { getSocket } from "@/lib/socket/client";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Leaderboard } from "./leaderboard";
+
+export function WarRoomLive({ messages: initialMessages, leaderboard }: { messages: WarRoomMessage[]; leaderboard: LeaderboardEntry[] }) {
+ const [messages, setMessages] = useState(initialMessages);
+ const [draft, setDraft] = useState("");
+
+ useEffect(() => {
+ const socket = getSocket();
+ const timer = window.setInterval(() => {
+ setMessages((items) => [
+ ...items.slice(-5),
+ {
+ id: crypto.randomUUID(),
+ author: "Defend Bot",
+ body: "Mock live prompt: compare behavior, not formatting.",
+ timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
+ kind: "defend",
+ },
+ ]);
+ }, 6000);
+
+ socket.connect();
+ return () => {
+ window.clearInterval(timer);
+ socket.disconnect();
+ };
+ }, []);
+
+ return (
+
+
+
+ Room feed
+
+
+
+ {messages.map((message) => (
+
+
+ {message.author}
+ {message.timestamp}
+
+
{message.body}
+
+ ))}
+
+
+ setDraft(event.target.value)} placeholder="Send a room note" />
+
+
+
+
+
+
+ );
+}
From d508103123f497d001ba85f72339b5c7974bef6d Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Sun, 28 Jun 2026 12:42:16 +0530
Subject: [PATCH 4/5] feat(web): add mock product pages
---
apps/web/src/app/app/blindspot-map/page.tsx | 45 ++++
apps/web/src/app/app/dashboard/page.tsx | 79 +++++++
apps/web/src/app/app/layout.tsx | 5 +
apps/web/src/app/app/page.tsx | 5 +
apps/web/src/app/app/profile/page.tsx | 47 ++++
.../[trackId]/modules/[moduleId]/page.tsx | 25 +++
apps/web/src/app/app/tracks/page.tsx | 52 +++++
apps/web/src/app/app/war-room/page.tsx | 25 +++
apps/web/src/app/auth/signin/page.tsx | 46 ++++
apps/web/src/app/auth/signup/page.tsx | 47 ++++
apps/web/src/app/page.tsx | 206 ++++++++++--------
11 files changed, 487 insertions(+), 95 deletions(-)
create mode 100644 apps/web/src/app/app/blindspot-map/page.tsx
create mode 100644 apps/web/src/app/app/dashboard/page.tsx
create mode 100644 apps/web/src/app/app/layout.tsx
create mode 100644 apps/web/src/app/app/page.tsx
create mode 100644 apps/web/src/app/app/profile/page.tsx
create mode 100644 apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
create mode 100644 apps/web/src/app/app/tracks/page.tsx
create mode 100644 apps/web/src/app/app/war-room/page.tsx
create mode 100644 apps/web/src/app/auth/signin/page.tsx
create mode 100644 apps/web/src/app/auth/signup/page.tsx
diff --git a/apps/web/src/app/app/blindspot-map/page.tsx b/apps/web/src/app/app/blindspot-map/page.tsx
new file mode 100644
index 0000000..bd5b9c4
--- /dev/null
+++ b/apps/web/src/app/app/blindspot-map/page.tsx
@@ -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 ;
+
+ return (
+ <>
+
+
+ {blindspots.map((blindspot) => (
+
+
+
+ {blindspot.concept}
+ 70 ? "warning" : "secondary"}>{blindspot.severity}% risk
+
+
+
+
+
+
+
Evidence
+
{blindspot.evidence}
+
+
+
Next action
+
{blindspot.nextAction}
+
+
+
+
+ ))}
+
+ >
+ );
+}
diff --git a/apps/web/src/app/app/dashboard/page.tsx b/apps/web/src/app/app/dashboard/page.tsx
new file mode 100644
index 0000000..4f9f725
--- /dev/null
+++ b/apps/web/src/app/app/dashboard/page.tsx
@@ -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 ;
+
+ 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 (
+ <>
+ Resume module}
+ />
+
+ {stats.map((stat) => {
+ const Icon = stat.icon;
+ return (
+
+
+
+ {stat.label}
+
+
+
+
+ {stat.value}
+ {stat.copy}
+
+
+ );
+ })}
+
+
+
+
+ {dashboard.activeTrack.title}
+
+
+ {dashboard.activeTrack.description}
+
+
+ {dashboard.activeTrack.modules.map((module) => (
+
+
{module.title}
+
{module.summary}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/app/layout.tsx b/apps/web/src/app/app/layout.tsx
new file mode 100644
index 0000000..3ba80ba
--- /dev/null
+++ b/apps/web/src/app/app/layout.tsx
@@ -0,0 +1,5 @@
+import { AppShell } from "@/components/app/app-shell";
+
+export default function ProductLayout({ children }: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/apps/web/src/app/app/page.tsx b/apps/web/src/app/app/page.tsx
new file mode 100644
index 0000000..2a301be
--- /dev/null
+++ b/apps/web/src/app/app/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function AppIndexPage() {
+ redirect("/app/dashboard");
+}
diff --git a/apps/web/src/app/app/profile/page.tsx b/apps/web/src/app/app/profile/page.tsx
new file mode 100644
index 0000000..e64d44a
--- /dev/null
+++ b/apps/web/src/app/app/profile/page.tsx
@@ -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 ;
+
+ return (
+ <>
+ IRS {profile.irs}} />
+
+
+
+
+
+
+
+ Session display
+
+
+ Name: {profile.name}
+ Role: {profile.role}
+ Completed modules: {profile.completedModules}
+
+
+
+
+ Recent modules
+
+
+ {profile.recent.map((item) => (
+ {item}
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx b/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
new file mode 100644
index 0000000..2e7d080
--- /dev/null
+++ b/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
@@ -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 ;
+
+ return (
+ <>
+ {data.module.concepts.map((item) => {item})}}
+ />
+
+ >
+ );
+}
diff --git a/apps/web/src/app/app/tracks/page.tsx b/apps/web/src/app/app/tracks/page.tsx
new file mode 100644
index 0000000..bf879f3
--- /dev/null
+++ b/apps/web/src/app/app/tracks/page.tsx
@@ -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 ;
+
+ return (
+ <>
+
+
+ {tracks.map((track) => (
+
+
+
+ {track.title}
+ {track.difficulty}
+
+
+
+ {track.description}
+
+
+ Progress
+ {track.progress}%
+
+
+
+
+ {track.modules.map((module) => (
+
+
{module.title}
+
+
+ ))}
+
+
+
+ ))}
+
+ >
+ );
+}
diff --git a/apps/web/src/app/app/war-room/page.tsx b/apps/web/src/app/app/war-room/page.tsx
new file mode 100644
index 0000000..c0922a4
--- /dev/null
+++ b/apps/web/src/app/app/war-room/page.tsx
@@ -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 ;
+
+ return (
+ <>
+ {data.room.members} members live}
+ />
+
+ >
+ );
+}
diff --git a/apps/web/src/app/auth/signin/page.tsx b/apps/web/src/app/auth/signin/page.tsx
new file mode 100644
index 0000000..b0cbf63
--- /dev/null
+++ b/apps/web/src/app/auth/signin/page.tsx
@@ -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 (
+
+
+
+
+ UV
+ UnVibe
+
+ Sign in
+ Use OAuth later, or enter the mock workspace now.
+
+
+
+
+
+
+
+
+
+ New here? Create an account
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx
new file mode 100644
index 0000000..eff1347
--- /dev/null
+++ b/apps/web/src/app/auth/signup/page.tsx
@@ -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 (
+
+
+
+
+ UV
+ UnVibe
+
+ Create account
+ Mock onboarding keeps frontend work unblocked.
+
+
+
+
+
+
+
+
+
+
+ Already training? Sign in
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 6fe62d1..18fb61d 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,101 +1,117 @@
-import Image from "next/image";
+import Link from "next/link";
+import { ArrowRight, Code2, GitBranch, Radar, ShieldQuestion, Swords } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
-export default function Home() {
+const signals = [
+ { label: "Decode", value: "annotate code intent", icon: Code2 },
+ { label: "Rebuild", value: "write from memory", icon: GitBranch },
+ { label: "Defend", value: "answer under pressure", icon: ShieldQuestion },
+];
+
+const featureCards = [
+ { title: "Blindspots", copy: "See weak concepts before they become habits.", icon: Radar },
+ { title: "War Rooms", copy: "Practice with peers in live competitive rooms.", icon: Swords },
+ { title: "Code memory", copy: "Submit rebuilds and compare against source.", icon: Code2 },
+];
+
+export default function LandingPage() {
return (
-
-
-
-
- -
- Get started by editing{" "}
-
- src/app/page.tsx
-
- .
-
- - Save and see your changes instantly.
-
+
+
+
+
-
-
-
- Deploy now
-
-
- Read our docs
-
+
+
+
+ AI learning loop for builders
+
+
+ Learn code until you can explain every decision.
+
+
+ UnVibe turns passive tutorials into an active sequence: read production code, rebuild it, then defend your reasoning in live sessions.
+
+
+
+
+
+
+
+
+
+
+
live module
+
Auth guard rebuild
+
+
IRS 82
+
+
+ {signals.map((signal, index) => {
+ const Icon = signal.icon;
+ return (
+
+
+
+
+
+
+
{signal.label}
+
0{index + 1}
+
+
{signal.value}
+
+
+ );
+ })}
+
+
+
const session = defend(rebuild);
+
score.update(session.reasoning);
+
{"// next question streams into War Room"}
+
+
-
-
-
+
+
+
+ {featureCards.map((feature) => {
+ const Icon = feature.icon;
+ return (
+
+
+
{feature.title}
+
{feature.copy}
+
+ );
+ })}
+
+
);
}
From 0241a23de36dc020bfce36649bbc6259e8b9525d Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Mon, 29 Jun 2026 13:09:00 +0530
Subject: [PATCH 5/5] feat: implement global dark/light mode system with
gradient backgrounds
- Add ThemeProvider wrapping root layout with dark/light radial gradients
- Add localStorage persistence for theme preference via Zustand store
- Add ThemeController toggle to landing page header
- Consolidate dark class toggling into ThemeProvider
- Remove opaque bg-background from body and page wrappers to reveal gradient
- Improve landing page spacing and move feature cards above the fold
- Ensure all pages (landing, dashboard, auth) respect global theme
---
apps/web/src/app/auth/signin/page.tsx | 2 +-
apps/web/src/app/auth/signup/page.tsx | 2 +-
apps/web/src/app/globals.css | 2 +-
apps/web/src/app/layout.tsx | 5 ++-
apps/web/src/app/page.tsx | 33 ++++++++++---------
apps/web/src/components/app/app-shell.tsx | 2 +-
.../src/components/app/theme-controller.tsx | 5 ---
.../web/src/components/app/theme-provider.tsx | 33 +++++++++++++++++++
apps/web/src/stores/ui-store.ts | 19 +++++++++--
9 files changed, 76 insertions(+), 27 deletions(-)
create mode 100644 apps/web/src/components/app/theme-provider.tsx
diff --git a/apps/web/src/app/auth/signin/page.tsx b/apps/web/src/app/auth/signin/page.tsx
index b0cbf63..7004db3 100644
--- a/apps/web/src/app/auth/signin/page.tsx
+++ b/apps/web/src/app/auth/signin/page.tsx
@@ -11,7 +11,7 @@ export default function SignInPage() {
const { signIn } = useAuthStore();
return (
-
+
diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx
index eff1347..7235c17 100644
--- a/apps/web/src/app/auth/signup/page.tsx
+++ b/apps/web/src/app/auth/signup/page.tsx
@@ -11,7 +11,7 @@ export default function SignUpPage() {
const { signIn } = useAuthStore();
return (
-
+
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 46aad00..d1209a4 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -71,7 +71,7 @@
@apply border-border;
}
body {
- @apply bg-background text-foreground;
+ @apply text-foreground;
}
::selection {
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 35a82c3..eb595a6 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import Providers from "./providers";
+import { ThemeProvider } from "@/components/app/theme-provider";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -30,7 +31,9 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
- {children}
+
+ {children}
+
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 18fb61d..ad0991d 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,7 +1,10 @@
+"use client";
+
import Link from "next/link";
import { ArrowRight, Code2, GitBranch, Radar, ShieldQuestion, Swords } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
+import { ThemeController } from "@/components/app/theme-controller";
const signals = [
{ label: "Decode", value: "annotate code intent", icon: Code2 },
@@ -17,9 +20,8 @@ const featureCards = [
export default function LandingPage() {
return (
-
+
-
-
+
AI learning loop for builders
@@ -98,19 +101,19 @@ export default function LandingPage() {
-
-
- {featureCards.map((feature) => {
- const Icon = feature.icon;
- return (
-
-
-
{feature.title}
-
{feature.copy}
-
- );
- })}
+
+ {featureCards.map((feature) => {
+ const Icon = feature.icon;
+ return (
+
+
+
{feature.title}
+
{feature.copy}
+
+ );
+ })}
+
);
diff --git a/apps/web/src/components/app/app-shell.tsx b/apps/web/src/components/app/app-shell.tsx
index 0019165..5bdd5d0 100644
--- a/apps/web/src/components/app/app-shell.tsx
+++ b/apps/web/src/components/app/app-shell.tsx
@@ -21,7 +21,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const { user, signOut } = useAuthStore();
return (
-
+