From 9e470979eaa24ca15e5afed3eba1b3eea13b7948 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:38:26 +0530
Subject: [PATCH 1/8] fix: await params in ModulePage via Server Component
wrapper (Next.js 15 compat)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
ModulePage was a Client Component destructuring params synchronously. In Next.js
15, params is a Promise. Since React 18 doesn't have use(), wrap the client
logic in a thin async Server Component that awaits params and passes them as
plain props to the inner Client Component.
No other files in the app/ tree destructure params or searchParams — this was
the only instance.
---
.../[moduleId]/module-page-content.tsx | 25 +++++++++++++++++
.../[trackId]/modules/[moduleId]/page.tsx | 27 +++----------------
2 files changed, 29 insertions(+), 23 deletions(-)
create mode 100644 apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/module-page-content.tsx
diff --git a/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/module-page-content.tsx b/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/module-page-content.tsx
new file mode 100644
index 0000000..9522e5f
--- /dev/null
+++ b/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/module-page-content.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 function ModulePageContent({ trackId, moduleId }: { trackId: string; moduleId: string }) {
+ const { data, isLoading } = useModuleQuery(trackId, moduleId);
+
+ if (isLoading || !data) return ;
+
+ return (
+ <>
+ {data.module.concepts.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
index 2e7d080..e26890c 100644
--- a/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
+++ b/apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx
@@ -1,25 +1,6 @@
-"use client";
+import { ModulePageContent } from "./module-page-content";
-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})}}
- />
-
- >
- );
+export default async function ModulePage({ params }: { params: Promise<{ trackId: string; moduleId: string }> }) {
+ const { trackId, moduleId } = await params;
+ return ;
}
From 6e0d3928fa8488c7a7c459f286bc1448a789450e Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:38:43 +0530
Subject: [PATCH 2/8] fix: prevent dark-mode flash on light-mode load
Remove hardcoded className="dark" from . Add blocking inline script
that reads localStorage("unvibe-theme") and sets the class before first
paint, matching the Zustand store's persistence strategy. Add
suppressHydrationWarning to to silence the false-positive mismatch
warning since the DOM is intentionally mutated before hydration.
---
apps/web/src/app/layout.tsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index eb595a6..9adcb50 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -26,10 +26,15 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
+
{children}
From 4c8994ce2bf9f099d2c01a4628a58bf904e6b681 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:38:59 +0530
Subject: [PATCH 3/8] fix: wire Monaco editor theme to UI store darkMode
Replace hardcoded theme="vs-dark" with a dynamic value read from
useUIStore.darkMode so the editor matches the app's light/dark setting.
---
apps/web/src/components/features/code-editor.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/features/code-editor.tsx b/apps/web/src/components/features/code-editor.tsx
index a332721..e7d2573 100644
--- a/apps/web/src/components/features/code-editor.tsx
+++ b/apps/web/src/components/features/code-editor.tsx
@@ -3,8 +3,11 @@
import Editor from "@monaco-editor/react";
import { RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { useUIStore } from "@/stores/ui-store";
export function CodeEditor({ code, language, onChange, onReset, readOnly = false }: { code: string; language: string; onChange: (value: string) => void; onReset?: () => void; readOnly?: boolean }) {
+ const darkMode = useUIStore((state) => state.darkMode);
+
return (
@@ -20,7 +23,7 @@ export function CodeEditor({ code, language, onChange, onReset, readOnly = false
height="420px"
language={language}
value={code}
- theme="vs-dark"
+ theme={darkMode ? "vs-dark" : "light"}
onChange={(value) => onChange(value ?? "")}
options={{
readOnly,
From 4ac870f04eda4cccc2335d277ba8c0dba896cbaa Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:39:24 +0530
Subject: [PATCH 4/8] fix: remove asChild/Link race condition in auth forms
Button with asChild wrapping a Link could cause onClick (signIn) to not fire
before navigation. Replace with a plain Button that calls signIn() then
router.push() in the handler. Add TODO noting this changes when real NextAuth
wiring lands.
---
apps/web/src/app/auth/signin/page.tsx | 7 +++++--
apps/web/src/app/auth/signup/page.tsx | 7 +++++--
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/apps/web/src/app/auth/signin/page.tsx b/apps/web/src/app/auth/signin/page.tsx
index 7004db3..ffc3f60 100644
--- a/apps/web/src/app/auth/signin/page.tsx
+++ b/apps/web/src/app/auth/signin/page.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import { Github, Mail } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -8,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";
export default function SignInPage() {
+ const router = useRouter();
const { signIn } = useAuthStore();
return (
@@ -32,8 +34,9 @@ export default function SignInPage() {
-
diff --git a/apps/web/src/app/auth/signup/page.tsx b/apps/web/src/app/auth/signup/page.tsx
index 7235c17..15ab9ce 100644
--- a/apps/web/src/app/auth/signup/page.tsx
+++ b/apps/web/src/app/auth/signup/page.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import { Github, Mail } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -8,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";
export default function SignUpPage() {
+ const router = useRouter();
const { signIn } = useAuthStore();
return (
@@ -33,8 +35,9 @@ export default function SignUpPage() {
-
- Create mock account
+ {/* TODO: when wired to real NextAuth signIn(), this becomes async and may handle its own redirect via callbackUrl — revisit the router.push() call then. */}
+ { signIn(); router.push('/app/dashboard'); }}>
+ Create mock account
From 9aa842f4c9e317dbbc8c3983abd90efd693d2619 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:39:41 +0530
Subject: [PATCH 5/8] chore: remove dead socket wiring in war-room-live
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Socket connect/disconnect was called but no socket events were listened
to — only a setInterval drove mock messages. Remove the socket calls and
unused getSocket import. Leave a TODO to wire real socket events when the
War Room backend lands.
---
apps/web/src/components/features/war-room-live.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/apps/web/src/components/features/war-room-live.tsx b/apps/web/src/components/features/war-room-live.tsx
index ceeb770..34fea87 100644
--- a/apps/web/src/components/features/war-room-live.tsx
+++ b/apps/web/src/components/features/war-room-live.tsx
@@ -2,7 +2,6 @@
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";
@@ -13,7 +12,7 @@ export function WarRoomLive({ messages: initialMessages, leaderboard }: { messag
const [draft, setDraft] = useState("");
useEffect(() => {
- const socket = getSocket();
+ // TODO: wire real socket events once War Room backend lands
const timer = window.setInterval(() => {
setMessages((items) => [
...items.slice(-5),
@@ -27,10 +26,8 @@ export function WarRoomLive({ messages: initialMessages, leaderboard }: { messag
]);
}, 6000);
- socket.connect();
return () => {
window.clearInterval(timer);
- socket.disconnect();
};
}, []);
From f56b01d3c11656b1f6e8650eb2d974cab298643f Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Thu, 2 Jul 2026 19:40:05 +0530
Subject: [PATCH 6/8] refactor: move hardcoded gradient hexes to CSS custom
properties
Define --gradient-radial in :root (light) and .dark (dark) in globals.css.
Replace the conditional inline style in ThemeProvider with a single
var(--gradient-radial) reference, which resolves automatically based on the
.dark class on .
---
apps/web/src/app/globals.css | 2 ++
apps/web/src/components/app/theme-provider.tsx | 13 +------------
2 files changed, 3 insertions(+), 12 deletions(-)
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index d1209a4..0ccb2e3 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -33,6 +33,7 @@
--ring: 188 91% 35%;
--radius: 0.5rem;
+ --gradient-radial: radial-gradient(125% 125% at 50% 90%, #ffffff 40%, #ec4899 100%);
}
.dark {
@@ -63,6 +64,7 @@
--border: 218 16% 21%;
--input: 218 16% 21%;
--ring: 187 85% 52%;
+ --gradient-radial: radial-gradient(125% 125% at 50% 100%, #000000 40%, #010133 100%);
}
}
diff --git a/apps/web/src/components/app/theme-provider.tsx b/apps/web/src/components/app/theme-provider.tsx
index 5c622c4..19ba202 100644
--- a/apps/web/src/components/app/theme-provider.tsx
+++ b/apps/web/src/components/app/theme-provider.tsx
@@ -14,18 +14,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
{children}
From 04645e558384dee4811cf072ade2366cfe0c5db9 Mon Sep 17 00:00:00 2001
From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com>
Date: Mon, 6 Jul 2026 22:03:46 +0530
Subject: [PATCH 7/8] fix: remove deprecated eyebrow prop from PageHeader in
module-page-content
Upstream removed the eyebrow prop from PageHeader component. Replace
eyebrow + description with single description that includes track title
as context prefix. Fixes TypeScript build error.
---
PR_DESCRIPTION.md | 277 ++++++++++++++++++
.../[moduleId]/module-page-content.tsx | 3 +-
2 files changed, 278 insertions(+), 2 deletions(-)
create mode 100644 PR_DESCRIPTION.md
diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md
new file mode 100644
index 0000000..6f26bab
--- /dev/null
+++ b/PR_DESCRIPTION.md
@@ -0,0 +1,277 @@
+## Title
+
+fix: address review feedback — params, theme flash, Monaco theme, auth race, socket cleanup, gradient tokens + merge upstream/main
+
+---
+
+## Summary
+
+Six fixes addressing the merge review feedback, plus a merge of upstream/main (7 file conflicts resolved). All six fixes are preserved after the merge. The branch is up to date with `upstream/main`, no remaining conflicts.
+
+---
+
+## Must-fix (blocking)
+
+### 1. `params` not awaited in ModulePage (Next.js 15 compat)
+
+**Commit:** `9e47097` | **File:** `apps/web/src/app/app/tracks/[trackId]/modules/[moduleId]/page.tsx`
+
+**Problem:** `ModulePage` was a Client Component destructuring `params` synchronously:
+```tsx
+export default function ModulePage({ params }: { params: { trackId: string; moduleId: string } }) {
+```
+
+In Next.js 15, `params` becomes a Promise. This code would break on upgrade.
+
+**Fix:** Since we're on React 18 (`use()` not available), we can't use `use(params)` in a Client Component. Instead, the page was split:
+- `page.tsx` → async Server Component that `await`s params and passes them as plain props
+- `module-page-content.tsx` → new Client Component (`"use client"`) receiving `{ trackId, moduleId }` as regular props
+
+This works identically on Next.js 14 today (awaiting a plain object resolves immediately) and will work on Next.js 15 after upgrade with zero changes.
+
+**Scope:** Grep of the entire `apps/web/src/app/` tree confirmed this was the **only file** destructuring `params` or `searchParams` — no other fixes needed.
+
+**Post-merge update:** After merging upstream/main, the inner Client Component was updated to use upstream's tRPC queries (`trpc.tracks.getById.useQuery`, `trpc.modules.getById.useQuery`) instead of the old mock data hooks, since upstream had already replaced the mock data layer.
+
+---
+
+### 2. Dark-mode flash on light-mode load
+
+**Commit:** `6e0d392` | **File:** `apps/web/src/app/layout.tsx`
+
+**Problem:** `` had a hardcoded `className="dark"`. The `ThemeProvider` toggles the `dark` class via `useEffect` — which runs *after* hydration. A user with a saved light-mode preference would see a dark page flash before React corrected it.
+
+**Root cause analysis:**
+- Theme preference is persisted via Zustand store → `localStorage("unvibe-theme")` → values `"dark"` or `"light"`
+- Zustand initialized on client only (hydration), so the server always rendered `className="dark"`
+- No inline script existed to read the stored preference before first paint
+
+**Fix (3-part):**
+
+1. **Removed `className="dark"`** from `` — the server no longer hardcodes dark mode
+2. **Added `suppressHydrationWarning`** to `` — silences the false-positive hydration mismatch warning. The warning would fire because our inline script (step 3) mutates the DOM before React hydrates, causing the server-rendered class and client class to differ. This is intentional and harmless — `suppressHydrationWarning` silences just that one attribute on ``.
+3. **Added blocking inline `