diff --git a/.claude/commands/coderabbit.md b/.claude/commands/coderabbit.md index e53a3a4..47d4085 100644 --- a/.claude/commands/coderabbit.md +++ b/.claude/commands/coderabbit.md @@ -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("
\\s*🧩 Analysis chain[\\s\\S]*?
\\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 | |-----------|----------| diff --git a/.claude/settings.json b/.claude/settings.json index 25b42b8..3e603bb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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", @@ -22,9 +35,10 @@ "Bash(cat:*)", "Bash(echo:*)", "Bash(find:*)", - "Bash(glob:*)", "Bash(lsof:*)", + "Bash(ls:*)", "Bash(sed:*)", + "Bash(tree:*)", "Bash(wc:*)", "Bash(xargs:*)", @@ -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)", @@ -58,10 +82,6 @@ "WebFetch(domain:ui.shadcn.com)", "WebFetch(domain:vercel.com)", "WebFetch(domain:vitest.dev)" - ], - - "deny": ["Read(**/.env)", "Read(**/.envrc)"], - - "ask": [] + ] } } diff --git a/.github/workflows/test-e2e-vercel.yml b/.github/workflows/test-e2e-vercel.yml index b0074f7..3f42a3b 100644 --- a/.github/workflows/test-e2e-vercel.yml +++ b/.github/workflows/test-e2e-vercel.yml @@ -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 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 5ff0221..eb76305 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -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 diff --git a/.gitignore b/.gitignore index 233d09b..0588baf 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ next-env.d.ts # Playwright .playwright/ .playwright-mcp/ + +# Temporary docs +x_docs/temp/ diff --git a/CLAUDE.md b/CLAUDE.md index 775e5c8..7d19afd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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
ID: {id}
; - }; - ``` +- 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 diff --git a/README.md b/README.md index 0b5fab0..e90806b 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..10b6e03 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,9 @@ +const AuthLayout = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +export default AuthLayout; diff --git a/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..bfb8f0e --- /dev/null +++ b/app/(auth)/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,12 @@ +import { SignIn } from "@clerk/nextjs"; +import { connection } from "next/server"; + +export default async function SignInPage() { + await connection(); + + return ( +
+ +
+ ); +} diff --git a/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..b1d6aac --- /dev/null +++ b/app/(auth)/sign-up/[[...sign-up]]/page.tsx @@ -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 ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx new file mode 100644 index 0000000..4892922 --- /dev/null +++ b/app/(root)/layout.tsx @@ -0,0 +1,12 @@ +import { Navbar } from "@/components/navigation/navbar"; + +const RootLayout = ({ children }: { children: React.ReactNode }) => { + return ( + <> + + {children} + + ); +}; + +export default RootLayout; diff --git a/app/page.tsx b/app/(root)/page.tsx similarity index 100% rename from app/page.tsx rename to app/(root)/page.tsx diff --git a/app/globals.css b/app/globals.css index cf673cf..5f8c12b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 *)); diff --git a/app/layout.tsx b/app/layout.tsx index 265fbf2..e624956 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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({ @@ -18,24 +30,23 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - - - {children} - - - - - + + + + + + {children} + + + + + ); } diff --git a/app/globals-old.css b/app/old.globals.css similarity index 94% rename from app/globals-old.css rename to app/old.globals.css index 5a27b19..1aff2bd 100644 --- a/app/globals-old.css +++ b/app/old.globals.css @@ -325,7 +325,7 @@ body { .active-theme { filter: invert(53%) sepia(98%) saturate(3332%) hue-rotate(0deg) - brightness(104%) contrast(106%) !important; + brightness(104%) contrast(106%); } .hash-span { @@ -335,23 +335,23 @@ body { } .mdxeditor-toolbar { - background: #ffffff !important; + background: #ffffff; } .dark .mdxeditor-toolbar { - background: #151821 !important; + background: #151821; } .dark .mdxeditor-toolbar button svg { - color: #858ead !important; + color: #858ead; } .dark .mdxeditor-toolbar button:hover svg { - color: #000 !important; + color: #000; } .dark .mdxeditor-toolbar [role="separator"] { - border-color: #555 !important; + border-color: #555; } .markdown a { @@ -365,9 +365,7 @@ code { word-wrap: break-word; -ms-word-break: break-all; - /* This is the dangerous one in WebKit, as it breaks things wherever */ - word-break: break-all; - /* Instead use this non-standard one: */ + /* Use break-word for better compatibility (non-standard but safer than break-all) */ word-break: break-word; /* Adds a hyphen where the word breaks, if supported (No Blink) */ @@ -377,7 +375,7 @@ code { hyphens: auto; padding: 2px; - color: #ff7000 !important; + color: #ff7000; } .markdown pre { @@ -390,10 +388,10 @@ code { display: block; overflow-x: auto; - color: inherit !important; + color: inherit; } [data-lexical-editor="true"] { - height: 350px !important; - overflow-y: auto !important; -} \ No newline at end of file + height: 350px; + overflow-y: auto; +} diff --git a/biome.json b/biome.json index fcde559..cf56d75 100644 --- a/biome.json +++ b/biome.json @@ -14,8 +14,7 @@ "!.next", "!dist", "!build", - "!x_docs/reference", - "!app/globals-old.css" + "!x_docs/reference" ] }, "css": { diff --git a/components.json b/components.json index 46c5316..ce09c89 100644 --- a/components.json +++ b/components.json @@ -18,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} + "registries": { + "@clerk": "https://clerk.com/r/{name}.json" + } } diff --git a/components/clerk-provider.tsx b/components/clerk-provider.tsx new file mode 100644 index 0000000..c30f7f5 --- /dev/null +++ b/components/clerk-provider.tsx @@ -0,0 +1,22 @@ +import { ClerkProvider as ClerkNextJSProvider } from "@clerk/nextjs"; +import { shadcn } from "@clerk/themes"; + +type ClerkProviderProps = React.ComponentProps; + +export function ClerkProvider({ + children, + appearance, + ...props +}: ClerkProviderProps) { + return ( + + {children} + + ); +} diff --git a/components/navigation/navbar/index.tsx b/components/navigation/navbar/index.tsx index 8ba76e3..5d3f7a4 100644 --- a/components/navigation/navbar/index.tsx +++ b/components/navigation/navbar/index.tsx @@ -1,14 +1,22 @@ +import { + SignedIn, + SignedOut, + SignInButton, + SignUpButton, + UserButton, +} from "@clerk/nextjs"; import Image from "next/image"; import Link from "next/link"; import { ThemeToggle } from "@/components/navigation/theme-toggle"; +import { Button } from "@/components/ui/button"; export const Navbar = () => ( ); diff --git a/components/navigation/theme-toggle.tsx b/components/navigation/theme-toggle.tsx index 3665de6..b922db9 100644 --- a/components/navigation/theme-toggle.tsx +++ b/components/navigation/theme-toggle.tsx @@ -12,19 +12,19 @@ export function ThemeToggle() { useEffect(() => setMounted(true), []); if (!mounted) { - return