diff --git a/.claude/settings.json b/.claude/settings.json index 3e603bb..4e595ab 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,23 +1,9 @@ { - "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", @@ -31,7 +17,6 @@ "mcp__playwright__browser_tabs", "mcp__playwright__browser_take_screenshot", "mcp__playwright__browser_wait_for", - "Bash(cat:*)", "Bash(echo:*)", "Bash(find:*)", @@ -41,13 +26,11 @@ "Bash(tree:*)", "Bash(wc:*)", "Bash(xargs:*)", - "Bash(git log:*)", "Bash(gh pr checks:*)", "Bash(gh pr list:*)", "Bash(gh run list:*)", "Bash(gh run view:*)", - "Bash(npm run build)", "Bash(npm run dev)", "Bash(npm run start)", @@ -61,7 +44,6 @@ "Bash(npx @biomejs/biome:*)", "Bash(npx lefthook:*)", "Bash(npx playwright:*)", - "Bash(vercel --help)", "Bash(vercel env --help)", "Bash(vercel env ls:*)", @@ -71,7 +53,6 @@ "Bash(vercel open)", "Bash(vercel project ls:*)", "Bash(vercel whoami)", - "WebFetch(domain:biomejs.dev)", "WebFetch(domain:docs.github.com)", "WebFetch(domain:github.com)", @@ -82,6 +63,36 @@ "WebFetch(domain:ui.shadcn.com)", "WebFetch(domain:vercel.com)", "WebFetch(domain:vitest.dev)" - ] + ], + "deny": ["Read(**/.env)", "Read(**/.envrc)"], + "ask": [] + }, + "enabledPlugins": { + "playwright-skill@playwright-skill": false, + "skill-creator@my-claude-plugins": false, + "frontend-design@my-claude-plugins": false, + "tailwindcss@my-claude-plugins": false, + "shadcn-ui@my-claude-plugins": true, + "feature-dev@claude-plugins-official": true + }, + "extraKnownMarketplaces": { + "playwright-skill": { + "source": { + "source": "github", + "repo": "lackeyjb/playwright-skill" + } + }, + "my-claude-plugins": { + "source": { + "source": "github", + "repo": "michellepace/my-claude-plugins" + } + }, + "claude-plugins-official": { + "source": { + "source": "github", + "repo": "anthropics/claude-plugins-official" + } + } } } diff --git a/CLAUDE.md b/CLAUDE.md index b035d8c..e41422b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # CLAUDE.md +**DevFlow** — A community-driven platform for asking and answering programming questions. Get help, share knowledge, and collaborate with developers from around the world. (Similar to Stack Overflow) + The project uses British English - strictly. ## Tech Stack @@ -29,6 +31,17 @@ vercel env ls # Check env vars are configured vercel whoami # Verify CLI is authenticated ``` +## shadcn/ui CLI + +```bash +npx shadcn@latest list @shadcn # List all available components +npx shadcn@latest search @shadcn -q "nav" # Search components by query +npx shadcn@latest view button card # Preview code before installing +npx shadcn@latest add # Add component to project +npx shadcn@latest add button --overwrite # Overwrite existing component +npx shadcn@latest add @v0/ # Add from v0.dev registry +``` + ## Coding Practices - Only add `"use client"` when interactivity is needed diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 10b6e03..6b5a4ed 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,6 +1,6 @@ const AuthLayout = ({ children }: { children: React.ReactNode }) => { return ( -
+
{children}
); diff --git a/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/sign-in/[[...sign-in]]/page.tsx index 703415d..1d879b5 100644 --- a/app/(auth)/sign-in/[[...sign-in]]/page.tsx +++ b/app/(auth)/sign-in/[[...sign-in]]/page.tsx @@ -4,9 +4,5 @@ import { ClerkSignIn } from "@/components/auth/clerk-signin"; export default async function SignInPage() { await connection(); - return ( -
- -
- ); + return ; } diff --git a/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/sign-up/[[...sign-up]]/page.tsx index feee066..bdd9d31 100644 --- a/app/(auth)/sign-up/[[...sign-up]]/page.tsx +++ b/app/(auth)/sign-up/[[...sign-up]]/page.tsx @@ -4,9 +4,5 @@ import { ClerkSignUp } from "@/components/auth/clerk-signup"; export default async function SignUpPage() { await connection(); - return ( -
- -
- ); + return ; } diff --git a/app/(root)/(no-right-sidebar)/community/page.tsx b/app/(root)/(no-right-sidebar)/community/page.tsx new file mode 100644 index 0000000..863d62f --- /dev/null +++ b/app/(root)/(no-right-sidebar)/community/page.tsx @@ -0,0 +1,3 @@ +export default function CommunityPage() { + return

Community

; +} diff --git a/app/(root)/(no-right-sidebar)/jobs/page.tsx b/app/(root)/(no-right-sidebar)/jobs/page.tsx new file mode 100644 index 0000000..ec6d850 --- /dev/null +++ b/app/(root)/(no-right-sidebar)/jobs/page.tsx @@ -0,0 +1,3 @@ +export default function JobsPage() { + return

Find Jobs

; +} diff --git a/app/(root)/(no-right-sidebar)/layout.tsx b/app/(root)/(no-right-sidebar)/layout.tsx new file mode 100644 index 0000000..34cae74 --- /dev/null +++ b/app/(root)/(no-right-sidebar)/layout.tsx @@ -0,0 +1,7 @@ +// Full-width layout without right sidebar +// Layout: [LeftSidebar (sticky)] [Main Content] +const NoRightSidebarLayout = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +export default NoRightSidebarLayout; diff --git a/app/(root)/(no-right-sidebar)/profile/page.tsx b/app/(root)/(no-right-sidebar)/profile/page.tsx new file mode 100644 index 0000000..e84a25b --- /dev/null +++ b/app/(root)/(no-right-sidebar)/profile/page.tsx @@ -0,0 +1,3 @@ +export default function ProfilePage() { + return

Profile

; +} diff --git a/app/(root)/(no-right-sidebar)/tags/page.tsx b/app/(root)/(no-right-sidebar)/tags/page.tsx new file mode 100644 index 0000000..4f47ff8 --- /dev/null +++ b/app/(root)/(no-right-sidebar)/tags/page.tsx @@ -0,0 +1,3 @@ +export default function TagsPage() { + return

Tags

; +} diff --git a/app/(root)/(with-right-sidebar)/ask-question/page.tsx b/app/(root)/(with-right-sidebar)/ask-question/page.tsx new file mode 100644 index 0000000..cc92fe1 --- /dev/null +++ b/app/(root)/(with-right-sidebar)/ask-question/page.tsx @@ -0,0 +1,3 @@ +export default function AskQuestionPage() { + return

Ask a Question

; +} diff --git a/app/(root)/(with-right-sidebar)/collections/page.tsx b/app/(root)/(with-right-sidebar)/collections/page.tsx new file mode 100644 index 0000000..2a084a1 --- /dev/null +++ b/app/(root)/(with-right-sidebar)/collections/page.tsx @@ -0,0 +1,3 @@ +export default function CollectionsPage() { + return

Collections

; +} diff --git a/app/(root)/(with-right-sidebar)/layout.tsx b/app/(root)/(with-right-sidebar)/layout.tsx new file mode 100644 index 0000000..ad0517f --- /dev/null +++ b/app/(root)/(with-right-sidebar)/layout.tsx @@ -0,0 +1,11 @@ +// TODO: Add for tag widgets, hot questions, etc. +// Layout: [LeftSidebar (sticky)] [Main Content] [RightSidebar] +const WithRightSidebarLayout = ({ + children, +}: { + children: React.ReactNode; +}) => { + return
{children}
; +}; + +export default WithRightSidebarLayout; diff --git a/app/(root)/(with-right-sidebar)/page.tsx b/app/(root)/(with-right-sidebar)/page.tsx new file mode 100644 index 0000000..4aa7c66 --- /dev/null +++ b/app/(root)/(with-right-sidebar)/page.tsx @@ -0,0 +1,31 @@ +const Home = () => ( + <> +

Hello Root page with heading H1

+ + {/* Header boundary marker */} +
HEADER: {"words ".repeat(50)}
+ + {/* Flexbox demo: grow vs flex-none */} +
+ {/* Fixed width - won't grow */} +
+ flex-none +
+ + {/* Grows to fill available space */} +
+ grow +
+ + {/* Fixed width - won't grow */} +
+ flex-none +
+
+ + {/* Footer boundary marker */} +
FOOTER: {"words ".repeat(50)}
+ +); + +export default Home; diff --git a/app/(root)/page.tsx b/app/(root)/page.tsx deleted file mode 100644 index c30eff3..0000000 --- a/app/(root)/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const Home = () => ( - <> -

Welcome to the world of Next.js

- -); - -export default Home; diff --git a/app/globals.css b/app/globals.css index ccf5d7f..80cc9ff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -29,6 +29,7 @@ --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); + --color-mobile-nav: var(--mobile-nav); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); @@ -54,7 +55,14 @@ :root { /* Creates CSS variables available to all elements (no utility generation) */ + /* Only defined in root, not .dark */ --radius: 0.625rem; + --primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */ + --gradient-primary: linear-gradient( + 93.22deg, + var(--primary) -13.95%, + var(--primary-gradient-to) 99.54% + ); --background: oklch(0.994 0 0); /* #FDFDFD */ --foreground: oklch(0.129 0.042 264.695); @@ -64,7 +72,7 @@ --popover-foreground: oklch(0.129 0.042 264.695); --primary: oklch(0.7089 0.1967 46.81); --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.968 0.007 247.896); + --secondary: oklch(0.9147 0.0205 264.47); /* #DCE3F1 - Figma */ --secondary-foreground: oklch(0.208 0.042 265.755); --muted: oklch(0.968 0.007 247.896); --muted-foreground: oklch(0.554 0.046 257.417); @@ -79,7 +87,7 @@ --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(1 0 0); /* #FFFFFF */ + --sidebar: oklch(1 0 0); /* Figma #ffffff */ --sidebar-foreground: oklch(0.2102 0.0185 270.39); /* #151821 */ --sidebar-primary: oklch(0.208 0.042 265.755); --sidebar-primary-foreground: oklch(0.984 0.003 247.858); @@ -87,15 +95,11 @@ --sidebar-accent-foreground: oklch(0.208 0.042 265.755); --sidebar-border: oklch(0.929 0.013 255.508); --sidebar-ring: oklch(0.704 0.04 256.788); - --primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */ - --gradient-primary: linear-gradient( - 93.22deg, - var(--primary) -13.95%, - var(--primary-gradient-to) 99.54% - ); + --mobile-nav: oklch(1 0 0); /* #FFFFFF */ /* Centralised logo theming */ --logo-full-themed: url("/images/logo-light.svg"); + --auth-bg: url("/images/auth-bg-light.webp"); } .dark { @@ -108,7 +112,7 @@ --popover-foreground: oklch(0.984 0.003 247.858); --primary: oklch(0.7089 0.1967 46.81); --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.279 0.041 260.031); + --secondary: oklch(0.2728 0.0257 265.4); /* #212734 - Figma */ --secondary-foreground: oklch(0.984 0.003 247.858); --muted: oklch(0.279 0.041 260.031); --muted-foreground: oklch(0.704 0.04 256.788); @@ -123,7 +127,7 @@ --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.178 0.013 270.6); /* #0F1117 */ + --sidebar: oklch(0.1783 0.0128 270.6); /* Figma #0F1117 */ --sidebar-foreground: oklch(1 0 0); /* #FFFFFF */ --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.984 0.003 247.858); @@ -131,9 +135,11 @@ --sidebar-accent-foreground: oklch(0.984 0.003 247.858); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.551 0.027 264.364); + --mobile-nav: oklch(0.1288 0.0406 264.7); /* #07080b */ /* Centralised logo theming (dark) */ --logo-full-themed: url("/images/logo-dark.svg"); + --auth-bg: url("/images/auth-bg-dark.webp"); } @layer base { @@ -187,3 +193,11 @@ @apply overflow-x-auto rounded-lg bg-muted p-4 text-sm; } } + +@layer utilities { + /* Custom SVG icons use hardcoded white fill. Since loads them as external + files, CSS currentColor won't work. Invert to black in light mode instead. */ + .invert-colors { + @apply invert dark:invert-0; + } +} diff --git a/app/old.globals.css b/app/old.globals.css deleted file mode 100644 index 1aff2bd..0000000 --- a/app/old.globals.css +++ /dev/null @@ -1,397 +0,0 @@ -@import "tailwindcss"; - -body { - font-family: "Inter", sans-serif; -} - -:root { - --radius: 8px; -} - -@layer utilities { - .background-light850_dark100 { - @apply bg-light-850 dark:bg-dark-100; - } - - .background-light900_dark200 { - @apply bg-light-900 dark:bg-dark-200; - } - - .background-light900_dark300 { - @apply bg-light-900 dark:bg-dark-300; - } - - .background-light800_darkgradient { - @apply bg-light-800 dark:dark-gradient; - } - - .background-light800_dark400 { - @apply bg-light-800 dark:bg-dark-400 !important; - } - - .background-light700_dark400 { - @apply bg-light-700 dark:bg-dark-400; - } - - .background-light700_dark300 { - @apply bg-light-700 dark:bg-dark-300; - } - - .background-light800_dark400 { - @apply bg-light-800 dark:bg-dark-400; - } - - .background-light800_dark300 { - @apply bg-light-800 dark:bg-dark-300 !important; - } - - .background-light800_dark200 { - @apply bg-light-800 dark:bg-dark-200; - } - - .background-dark400_light900 { - @apply dark:bg-dark-400 bg-light-900 !important; - } - - .text-dark100_light900 { - @apply text-dark-100 dark:text-light-900 !important; - } - - .text-dark200_light900 { - @apply text-dark-200 dark:text-light-900; - } - - .text-dark200_light800 { - @apply text-dark-200 dark:text-light-800 !important; - } - - .text-dark300_light700 { - @apply text-dark-300 dark:text-light-700; - } - - .text-dark400_light700 { - @apply text-dark-400 dark:text-light-700; - } - - .text-dark500_light700 { - @apply text-dark-500 dark:text-light-700 !important; - } - - .text-dark500_light500 { - @apply text-dark-500 dark:text-light-500; - } - - .text-dark500_light400 { - @apply text-dark-500 dark:text-light-400; - } - - .text-dark300_light900 { - @apply text-dark-300 dark:text-light-900 !important; - } - - .text-dark400_light800 { - @apply text-dark-400 dark:text-light-800; - } - - .text-light400_light500 { - @apply text-light-400 dark:text-light-500 !important; - } - - .text-dark400_light500 { - @apply text-dark-400 dark:text-light-500; - } - - .text-dark400_light900 { - @apply text-dark-400 dark:text-light-900 !important; - } - - .text-light400_light500 { - @apply text-light-400 dark:text-light-500 !important; - } - - .light-border { - @apply border-light-800 dark:border-dark-300; - } - - .light-border-2 { - @apply border-light-700 dark:border-dark-400 !important; - } - - .h1-bold { - @apply text-[30px] font-bold leading-[42px] tracking-tighter; - } - - .h2-bold { - @apply text-[24px] font-bold leading-[31.2px]; - } - - .h2-semibold { - @apply text-[24px] font-semibold leading-[31.2px]; - } - - .h3-bold { - @apply text-[20px] font-bold leading-[26px]; - } - - .h3-semibold { - @apply text-[20px] font-semibold leading-[24.8px]; - } - - .base-medium { - @apply text-[18px] font-medium leading-[25.2px]; - } - - .base-semibold { - @apply text-[18px] font-semibold leading-[25.2px]; - } - - .base-bold { - @apply text-[18px] font-bold leading-[140%]; - } - - .paragraph-regular { - @apply text-[16px] font-normal leading-[22.4px]; - } - - .paragraph-medium { - @apply text-[16px] font-medium leading-[22.4px]; - } - - .paragraph-semibold { - @apply text-[16px] font-semibold leading-[20.8px]; - } - - .body-regular { - @apply text-[14px] font-normal leading-[19.6px]; - } - - .body-medium { - @apply text-[14px] font-medium leading-[18.2px]; - } - - .body-semibold { - @apply text-[14px] font-semibold leading-[18.2px]; - } - - .body-bold { - @apply text-[14px] font-bold leading-[18.2px]; - } - - .small-regular { - @apply text-[12px] font-normal leading-[15.6px]; - } - - .small-medium { - @apply text-[12px] font-medium leading-[15.6px]; - } - - .small-semibold { - @apply text-[12px] font-semibold leading-[15.6px]; - } - - .subtle-medium { - @apply text-[10px] font-medium leading-[13px] !important; - } - - .subtle-regular { - @apply text-[10px] font-normal leading-[13px]; - } - - .placeholder { - @apply placeholder:text-light-400 dark:placeholder:text-light-500; - } - - .invert-colors { - @apply invert dark:invert-0; - } - - .shadow-light100_dark100 { - @apply shadow-light-100 dark:shadow-dark-100; - } - - .shadow-light100_darknone { - @apply shadow-light-100 dark:shadow-none; - } - - .primary-gradient { - background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); - } - - .dark-gradient { - background: linear-gradient( - 232deg, - rgba(23, 28, 35, 0.41) 0%, - rgba(19, 22, 28, 0.7) 100% - ); - } - - .light-gradient { - background: linear-gradient( - 132deg, - rgba(247, 249, 255, 0.5) 0%, - rgba(229, 237, 255, 0.25) 100% - ); - } - - .primary-text-gradient { - background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } - - .flex-center { - @apply flex justify-center items-center; - } - - .flex-between { - @apply flex justify-between items-center; - } - - .flex-start { - @apply flex justify-start items-center; - } - - .card-wrapper { - @apply bg-light-900 dark:dark-gradient shadow-light-100 dark:shadow-dark-100; - } - - .btn { - @apply bg-light-800 dark:bg-dark-300 !important; - } - - .btn-secondary { - @apply bg-light-800 dark:bg-dark-400 !important; - } - - .btn-tertiary { - @apply bg-light-700 dark:bg-dark-300 !important; - } - - .no-focus { - @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important; - } - - .markdown { - @apply max-w-full prose dark:prose-p:text-light-700 dark:prose-ol:text-light-700 dark:prose-ul:text-light-500 dark:prose-strong:text-white dark:prose-headings:text-white prose-headings:text-dark-400 prose-h1:text-dark-300 prose-h2:text-dark-300 prose-p:text-dark-500 prose-ul:text-dark-500 prose-ol:text-dark-500; - } - - .markdown-editor { - @apply prose max-w-full prose-p:m-0 dark:prose-headings:text-white prose-headings:text-dark-400 prose-p:text-dark-500 dark:prose-p:text-light-700 prose-ul:text-dark-500 dark:prose-ul:text-light-700 prose-ol:text-dark-500 dark:prose-ol:text-light-700 dark:prose-strong:text-white prose-blockquote:text-dark-500 dark:prose-blockquote:text-light-700; - } - - .tab { - @apply min-h-full dark:bg-dark-400 bg-light-800 text-light-500 dark:data-[state=active]:bg-dark-300 data-[state=active]:bg-primary-100 data-[state=active]:text-primary-500 !important; - } - - .dark-gradient { - background: linear-gradient( - 232deg, - rgba(23, 28, 35, 0.41) 0%, - rgba(19, 22, 28, 0.7) 100% - ); - } -} - -.custom-scrollbar::-webkit-scrollbar { - width: 3px; - height: 3px; - border-radius: 2px; -} - -.custom-scrollbar::-webkit-scrollbar-track { - background: #ffffff; -} - -.custom-scrollbar::-webkit-scrollbar-thumb { - background: #888; - border-radius: 50px; -} - -.custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: #555; -} - -/* Hide scrollbar for Chrome, Safari and Opera */ -.no-scrollbar::-webkit-scrollbar { - display: none; -} - -/* Hide scrollbar for IE, Edge and Firefox */ -.no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} - -.active-theme { - filter: invert(53%) sepia(98%) saturate(3332%) hue-rotate(0deg) - brightness(104%) contrast(106%); -} - -.hash-span { - margin-top: -140px; - padding-bottom: 140px; - display: block; -} - -.mdxeditor-toolbar { - background: #ffffff; -} - -.dark .mdxeditor-toolbar { - background: #151821; -} - -.dark .mdxeditor-toolbar button svg { - color: #858ead; -} - -.dark .mdxeditor-toolbar button:hover svg { - color: #000; -} - -.dark .mdxeditor-toolbar [role="separator"] { - border-color: #555; -} - -.markdown a { - color: #1da1f2; -} - -.markdown a, -code { - /* These are technically the same, but use both */ - overflow-wrap: break-word; - word-wrap: break-word; - - -ms-word-break: break-all; - /* 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) */ - -ms-hyphens: auto; - -moz-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; - - padding: 2px; - color: #ff7000; -} - -.markdown pre { - display: grid; - width: 100%; -} - -.markdown pre code { - width: 100%; - display: block; - overflow-x: auto; - - color: inherit; -} - -[data-lexical-editor="true"] { - height: 350px; - overflow-y: auto; -} diff --git a/components/navigation/constants.ts b/components/navigation/constants.ts new file mode 100644 index 0000000..1a66ff9 --- /dev/null +++ b/components/navigation/constants.ts @@ -0,0 +1,19 @@ +export type NavLink = { + imgURL: string; + route: string; + label: string; +}; + +export const NAV_LINKS = [ + { imgURL: "/icons/home.svg", route: "/", label: "Home" }, + { imgURL: "/icons/users.svg", route: "/community", label: "Community" }, + { imgURL: "/icons/star.svg", route: "/collections", label: "Collections" }, + { imgURL: "/icons/suitcase.svg", route: "/jobs", label: "Find Jobs" }, + { imgURL: "/icons/tag.svg", route: "/tags", label: "Tags" }, + { + imgURL: "/icons/question.svg", + route: "/ask-question", + label: "Ask a question", + }, + { imgURL: "/icons/user.svg", route: "/profile", label: "Profile" }, +] as const satisfies readonly NavLink[]; diff --git a/components/navigation/full-logo.tsx b/components/navigation/full-logo.tsx new file mode 100644 index 0000000..0c663a0 --- /dev/null +++ b/components/navigation/full-logo.tsx @@ -0,0 +1,27 @@ +import { cn } from "@/lib/utils"; + +// Logo SVG dimensions - centralised to avoid magic numbers +const LOGO_WIDTH = 137; +const LOGO_HEIGHT = 23; + +interface ThemeLogoProps { + className?: string; +} + +/** + * Theme-aware logo that switches between light/dark variants via CSS variable. + * Uses --logo-full-themed defined in globals.css. + */ +export function ThemeLogo({ className }: ThemeLogoProps) { + return ( + + ); +} diff --git a/components/navigation/mobile-nav.tsx b/components/navigation/mobile-nav.tsx new file mode 100644 index 0000000..947fd6e --- /dev/null +++ b/components/navigation/mobile-nav.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { + SignedIn, + SignedOut, + SignInButton, + SignOutButton, + SignUpButton, +} from "@clerk/nextjs"; +import { LogOut, Menu, X } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { NAV_LINKS } from "@/components/navigation/constants"; +import { ThemeLogo } from "@/components/navigation/full-logo"; +import { NavLink } from "@/components/navigation/nav-link"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +export function MobileNav() { + const [open, setOpen] = useState(false); + + return ( + + + + + + {/* Visually hidden title and description for accessibility */} + Navigation menu + + Browse site pages and manage your account + + + {/* Logo */} + + + + + + + {/* Navigation Links */} + + + {/* Sign out - Only when signed in */} + +
+ + + + + +
+
+ + {/* Auth Buttons - Only when signed out */} + +
+ + + + + + + + + + +
+
+
+
+ ); +} diff --git a/components/navigation/nav-link.tsx b/components/navigation/nav-link.tsx new file mode 100644 index 0000000..6cc5698 --- /dev/null +++ b/components/navigation/nav-link.tsx @@ -0,0 +1,41 @@ +"use client"; + +import type { Route } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import type { NavLink as NavLinkType } from "@/components/navigation/constants"; +import { cn } from "@/lib/utils"; + +type NavLinkProps = NavLinkType & { + // Passed by SheetClose asChild to close the sheet on click + onClick?: () => void; +}; + +export function NavLink({ imgURL, route, label, onClick }: NavLinkProps) { + const pathname = usePathname(); + const isActive = + (pathname.startsWith(route) && route.length > 1) || pathname === route; + + return ( + + {label} + {label} + + ); +} diff --git a/components/navigation/navbar/index.tsx b/components/navigation/navbar.tsx similarity index 51% rename from components/navigation/navbar/index.tsx rename to components/navigation/navbar.tsx index eeedaf7..1bc00a3 100644 --- a/components/navigation/navbar/index.tsx +++ b/components/navigation/navbar.tsx @@ -7,13 +7,15 @@ import { } from "@clerk/nextjs"; import Image from "next/image"; import Link from "next/link"; +import { ThemeLogo } from "@/components/navigation/full-logo"; +import { MobileNav } from "@/components/navigation/mobile-nav"; 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 b922db9..3901ec6 100644 --- a/components/navigation/theme-toggle.tsx +++ b/components/navigation/theme-toggle.tsx @@ -12,13 +12,13 @@ export function ThemeToggle() { useEffect(() => setMounted(true), []); if (!mounted) { - return