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
6 changes: 4 additions & 2 deletions .claude/commands/coderabbit.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ Parse `$1` to extract owner, repo, and comment ID.
# strips analysis chain
gh api repos/OWNER/REPO/pulls/comments/COMMENT_ID \
--jq '.body | gsub("<details>\\s*<summary>🧩 Analysis chain</summary>[\\s\\S]*?</details>\\s*"; "")' \
> z_comment.md
> z_rabbit_comment.md
```

**Important:** Write `z_rabbit_comment.md` to the project root (current working directory), not `/tmp/`.

## 2. Evaluate

CodeRabbit AI is not always right.

Evaluate the comment `z_comment.md` against the context of our codebase and files it references. Assess:
Evaluate the comment `z_rabbit_comment.md` against the context of our codebase and files it references. Assess:

| Criterion | Question |
|-----------|----------|
Expand Down
32 changes: 26 additions & 6 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
{
"extraKnownMarketplaces": {
"playwright-skill": {
"source": {
"source": "github",
"repo": "lackeyjb/playwright-skill"
}
}
},

"permissions": {
"deny": ["Read(**/.env)", "Read(**/.envrc)"],
"ask": [],

"allow": [
"mcp__ide__getDiagnostics",

"Bash(claude mcp get:*)",
"Bash(claude mcp list)",

"mcp__playwright__browser_click",
"mcp__playwright__browser_close",
"mcp__playwright__browser_console_messages",
"mcp__playwright__browser_evaluate",
Expand All @@ -22,9 +35,10 @@
"Bash(cat:*)",
"Bash(echo:*)",
"Bash(find:*)",
"Bash(glob:*)",
"Bash(lsof:*)",
"Bash(ls:*)",
"Bash(sed:*)",
"Bash(tree:*)",
"Bash(wc:*)",
"Bash(xargs:*)",

Expand All @@ -48,6 +62,16 @@
"Bash(npx lefthook:*)",
"Bash(npx playwright:*)",

"Bash(vercel --help)",
"Bash(vercel env --help)",
"Bash(vercel env ls:*)",
"Bash(vercel git --help)",
"Bash(vercel integration --help)",
"Bash(vercel list:*)",
"Bash(vercel open)",
"Bash(vercel project ls:*)",
"Bash(vercel whoami)",

"WebFetch(domain:biomejs.dev)",
"WebFetch(domain:docs.github.com)",
"WebFetch(domain:github.com)",
Expand All @@ -58,10 +82,6 @@
"WebFetch(domain:ui.shadcn.com)",
"WebFetch(domain:vercel.com)",
"WebFetch(domain:vitest.dev)"
],

"deny": ["Read(**/.env)", "Read(**/.envrc)"],

"ask": []
]
}
}
6 changes: 6 additions & 0 deletions .github/workflows/test-e2e-vercel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ jobs:
# Bypass secret for Vercel Deployment Protection (required when protection is enabled)
# This allows Playwright to access protected preview deployments
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
# Clerk auth testing variables
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
E2E_TEST_EMAIL: ${{ secrets.E2E_TEST_EMAIL }}
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
E2E_TEST_OTP: ${{ secrets.E2E_TEST_OTP }}

# Step: Upload test report
- uses: actions/upload-artifact@v5
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,19 @@ jobs:
# Step: Build production
- name: Build Next.js production
run: npx next build
env:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}

# Step: Run E2E tests
- name: Run Playwright tests
run: npx playwright test
env:
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
E2E_TEST_EMAIL: ${{ secrets.E2E_TEST_EMAIL }}
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
E2E_TEST_OTP: ${{ secrets.E2E_TEST_OTP }}

# Step: Upload test report
- uses: actions/upload-artifact@v5
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ next-env.d.ts
# Playwright
.playwright/
.playwright-mcp/

# Temporary docs
x_docs/temp/
14 changes: 5 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ npm run test:unit # Vitest
npm run test:e2e # Playwright
npm run test # All tests (Vitest + Playwright)

vercel list # List project deployments
vercel list # Recent deployments and status
vercel env ls # Check env vars are configured
vercel whoami # Verify CLI is authenticated
```

## Coding Practices
Expand All @@ -35,14 +37,8 @@ vercel list # List project deployments
## Breaking Changes

- Tailwind v4 uses `@import "tailwindcss"` syntax (not `@tailwind` directives)
- Dynamic route `params` is a Promise - must be awaited in page components:

```tsx
const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
return <div>ID: {id}</div>;
};
```
- Next.js 16 Dynamic route `params` is a Promise - must await: `{ params }: { params: Promise<{ id: string }> }`
- Next.js 16 Middleware renamed to Proxy - `middleware.ts` → `proxy.ts`, export `proxy()` not `middleware()`

## Common Additions for New Projects

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Next.js 16 with the below:
| Testing | [Vitest](https://vitest.dev) | Fast unit test runner (Vite-native, Jest-compatible) |
| | [Playwright](https://playwright.dev) | E2E browser testing (Chromium, Firefox, WebKit, Mobile) |
| | [Testing Library](https://testing-library.com) | React component testing utilities |
| Git Hooks | [Lefthook](https://lefthook.dev) | Runs checks on commit/push (lint, typecheck, tests) |
| Git Hooks | [Lefthook](https://lefthook.dev) | Runs checks on commit (lint, typecheck, unit tests) and push (build, E2E tests) |
| Optimisation | [React Compiler](https://react.dev/learn/react-compiler) | Automatic memoisation and performance optimisations |
| Analytics | [Vercel Speed Insights](https://vercel.com/docs/speed-insights) | Real user performance metrics viewable on Vercel |
| | [Vercel Web Analytics](https://vercel.com/docs/analytics) | Privacy-friendly visitor analytics viewable on Vercel |
Expand Down
9 changes: 9 additions & 0 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return (
<main className="min-h-screen bg-cover bg-center bg-no-repeat bg-[url('/images/auth-bg-light.webp')] dark:bg-[url('/images/auth-bg-dark.webp')]">
{children}
</main>
);
};

export default AuthLayout;
12 changes: 12 additions & 0 deletions app/(auth)/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SignIn } from "@clerk/nextjs";
import { connection } from "next/server";

export default async function SignInPage() {
await connection();

return (
<div className="flex min-h-screen items-center justify-center p-6">
<SignIn />
</div>
);
}
57 changes: 57 additions & 0 deletions app/(auth)/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { SignUp } from "@clerk/nextjs";
import { ChartLine, Clock, ShieldCheck, Sparkles } from "lucide-react";
import { connection } from "next/server";

export default async function SignUpPage() {
await connection();
return (
<div className="grid min-h-screen lg:grid-cols-2">
<div className="hidden flex-1 items-center justify-end p-6 md:p-10 lg:flex">
<ul className="max-w-sm space-y-8">
<li>
<div className="flex items-center gap-2">
<Clock className="size-4" aria-hidden="true" />
<p className="font-semibold">Save on development time</p>
</div>
<p className="text-muted-foreground mt-2 text-sm">
Add authentication and user management to your app with just a few
lines of code.
</p>
</li>
<li>
<div className="flex items-center gap-2">
<ChartLine className="size-4" aria-hidden="true" />
<p className="font-semibold">Increase engagement</p>
</div>
<p className="text-muted-foreground mt-2 text-sm">
Add intuitive UIs designed to decrease friction for your users.
</p>
</li>
<li>
<div className="flex items-center gap-2">
<ShieldCheck className="size-4" aria-hidden="true" />
<p className="font-semibold">Protect your users</p>
</div>
<p className="text-muted-foreground mt-2 text-sm">
Enable features like two-step verification and enjoy automatic
security updates.
</p>
</li>
<li>
<div className="flex items-center gap-2">
<Sparkles className="size-4" aria-hidden="true" />
<p className="font-semibold">Match your brand</p>
</div>
<p className="text-muted-foreground mt-2 text-sm">
Theme our pre-built components, or integrate with our easy-to-use
APIs.
</p>
</li>
</ul>
</div>
<div className="flex flex-1 items-center justify-center p-6 md:p-10 lg:justify-start">
<SignUp />
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions app/(root)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Navbar } from "@/components/navigation/navbar";

const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
</>
);
};

export default RootLayout;
File renamed without changes.
1 change: 1 addition & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@clerk/themes/shadcn.css";

/* Applies dark: styles when element is .dark or inside .dark */
@custom-variant dark (&:where(.dark, .dark *));
Expand Down
61 changes: 36 additions & 25 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next";
import { inter, jetbrainsMono, spaceGrotesk } from "@/app/fonts";
import { Navbar } from "@/components/navigation/navbar";
import { Geist, Geist_Mono } from "next/font/google";
import { Suspense } from "react";
import { ClerkProvider } from "@/components/clerk-provider";
import { ThemeProvider } from "@/components/theme-provider";
import "@/app/globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Devflow",
title: "DevFlow",
description:
"A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world. Explore topics in web development, mobile app development, algorithms, data structures, and more.",
"A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world.",
icons: {
icon: "/images/site-logo.svg",
},
};

export default function RootLayout({
Expand All @@ -18,24 +30,23 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="en-GB"
suppressHydrationWarning
className={`${inter.variable} ${spaceGrotesk.variable} ${jetbrainsMono.variable}`}
>
<body className="antialiased">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Navbar />
{children}
</ThemeProvider>
<SpeedInsights />
<Analytics />
</body>
</html>
<Suspense fallback={null}>
<ClerkProvider>
<html lang="en" className="h-full" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} flex min-h-full flex-col antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</ClerkProvider>
</Suspense>
Comment on lines +33 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "layout.tsx" -path "*/app/*" | head -20

Repository: michellepace/devflow

Length of output: 128


🏁 Script executed:

cat -n ./app/layout.tsx

Repository: michellepace/devflow

Length of output: 1848


🌐 Web query:

Next.js 16 root layout Suspense html body top-level requirements

💡 Result:

Short answer — in Next.js (App Router / root layout):

  • The app directory must include a root layout file (app/layout.js|ts|tsx). It is the top-most layout that applies to all routes. (nextjs.org)
  • The root layout must explicitly render and (Next.js does not auto-create them). Those tags should be the top-level elements returned by the root layout. (nextjs.org)
  • Root layouts are Server Components by default and cannot be a Client Component (i.e., don't use "use client" at the root layout). (nextjs.org)
  • Do not manually manage contents inside the root layout; use the Metadata API (Next.js handles streaming / dedupe of head elements). (nextjs.org)
  • You can use React Suspense inside layouts/pages. Newer React features allow suspending rendering while streaming; but the root layout still must include the top-level and elements (you can place Suspense around children or nested parts as needed). For the root-level constraints rely on the Next.js root-layout rules above when using Suspense. (nextjs.org)

If you want examples (root layout with Suspense or wrapping in Suspense), tell me and I’ll show a minimal code sample.

Citations:


🌐 Web query:

Next.js 16 root layout Suspense inside body ClerkProvider valid

💡 Result:

Short answer: Yes — you can put a React Suspense boundary inside the of your Next.js 16 root layout, and you can wrap your app with ClerkProvider there — but you must respect server/client component rules.

Key points and recommended patterns

  • The root layout must return and and is a Server Component by default. Put global HTML there. [1].
  • Suspense/fallbacks are supported in the App Router and may be used in the root layout to provide a fallback while server data or client boundaries load (or to avoid the "uncached data accessed outside " error). Use React.Suspense around components that need fallbacks or will suspend. [2][3].
  • ClerkProvider is part of Clerk’s Next.js integration and is typically used to provide auth context for client usage. Clerk’s docs show wrapping layouts with ClerkProvider (often inside a Suspense) and explain options like using or marking the provider as a client component so it behaves correctly with the App Router streaming model. Follow Clerk’s examples (wrap in Suspense when needed, or use ClerkProvider.dynamic when you need to opt a layout into dynamic rendering). [4][5].

Practical guidance

  • Keep app/layout.tsx as the root layout (server). If ClerkProvider is a client component:
    • Create a small client wrapper (add "use client") that renders {children}, and import that wrapper into the server root layout; OR
    • Use Clerk’s documented dynamic option (e.g. ) per Clerk’s guide to opt the layout/route into dynamic rendering and wrap it in a Suspense fallback. [4][5].
  • Place Suspense around the client/provider boundary or components that suspend; fallback UI will render in while the client boundary hydrates. [2][3].

Sources

  • Next.js App Router / root layout docs. [1].
  • Next.js guidance on Suspense and providing fallbacks (uncached data message). [2][3].
  • Clerk Next.js integration / rendering modes and examples (ClerkProvider + Suspense + dynamic). [4][5].

If you want, tell me whether you’re using Clerk’s latest package and I’ll show the exact minimal wrapper/layout code for your setup.


🏁 Script executed:

find . -name "*clerk*provider*" -type f

Repository: michellepace/devflow

Length of output: 95


🏁 Script executed:

cat -n ./components/clerk-provider.tsx

Repository: michellepace/devflow

Length of output: 684


Move <Suspense> inside <body> to maintain proper root layout structure

Next.js root layouts must return <html> and <body> as the top-level elements. Currently, <Suspense> wraps these tags, which violates this requirement and may cause validation errors or build failures.

Restructure to keep <html>/<body> at the root while wrapping ClerkProvider and nested content in Suspense:

  return (
+   <html lang="en" className="h-full" suppressHydrationWarning>
+     <body
+       className={`${geistSans.variable} ${geistMono.variable} flex min-h-full flex-col antialiased`}
+     >
      <Suspense fallback={null}>
        <ClerkProvider>
-         <html lang="en" className="h-full" suppressHydrationWarning>
-           <body
-             className={`${geistSans.variable} ${geistMono.variable} flex min-h-full flex-col antialiased`}
-           >
            <ThemeProvider
              attribute="class"
              defaultTheme="system"
              enableSystem
              disableTransitionOnChange
            >
              {children}
            </ThemeProvider>
-           </body>
-         </html>
        </ClerkProvider>
       </Suspense>
+     </body>
+   </html>
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Suspense fallback={null}>
<ClerkProvider>
<html lang="en" className="h-full" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} flex min-h-full flex-col antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</ClerkProvider>
</Suspense>
<html lang="en" className="h-full" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} flex min-h-full flex-col antialiased`}
>
<Suspense fallback={null}>
<ClerkProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</ClerkProvider>
</Suspense>
</body>
</html>

);
}
Loading
Loading