please add dark and light theme switch

This commit is contained in:
Indigo Tang 2025-06-09 03:10:40 +00:00
parent 606d2adb96
commit 128334e790
6 changed files with 145 additions and 66 deletions

11
package-lock.json generated
View File

@ -39,6 +39,7 @@
"genkit": "^1.8.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
@ -7653,6 +7654,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
"integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18",
"react-dom": "^16.8 || ^17 || ^18"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@ -43,6 +43,7 @@
"genkit": "^1.8.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",

View File

@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/theme-provider';
const ptSans = PT_Sans({
subsets: ['latin'],
@ -36,12 +37,19 @@ export default function RootLayout({
className={cn('min-h-screen bg-background font-body antialiased flex flex-col', ptSans.variable)}
suppressHydrationWarning={true}
>
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
<Toaster />
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
<Toaster />
</ThemeProvider>
</body>
</html>
);

View File

@ -13,25 +13,21 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useState, useEffect } from 'react';
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
export default function Header() {
const pathname = usePathname();
// Mock authentication state
const [isAuthenticated, setIsAuthenticated] = useState(false);
// This effect runs only on the client after hydration
useEffect(() => {
// In a real app, you'd check localStorage or an auth context
// For now, we'll simulate it.
// To test logged-in state, you can manually set this in browser console: localStorage.setItem('isToyShareAuthenticated', 'true');
const authStatus = localStorage.getItem('isToyShareAuthenticated');
setIsAuthenticated(authStatus === 'true');
}, [pathname]); // Re-check on path change, e.g. after login/logout actions
}, [pathname]);
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated');
setIsAuthenticated(false);
// Potentially redirect to home or login page
// router.push('/'); // if using useRouter
// router.push('/'); // Consider redirecting if not already handled by auth checks elsewhere
};
@ -42,58 +38,62 @@ export default function Header() {
<ToyBrick className="h-7 w-7" />
<h1 className="text-2xl font-headline font-bold">ToyShare</h1>
</Link>
<nav className="flex items-center gap-4">
<Link href="/" passHref>
<Button variant={pathname === '/' ? 'secondary' : 'ghost'} size="sm">
Browse Toys
</Button>
</Link>
{isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<UserCircle2 className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/profile">
<UserCircle2 className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogIn className="mr-2 h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<>
<Link href="/login" passHref>
<Button variant={pathname === '/login' ? 'secondary' : 'ghost'} size="sm">
<LogIn className="mr-2 h-4 w-4" />
Login
</Button>
</Link>
<Link href="/register" passHref>
<Button variant="default" size="sm">
<UserPlus className="mr-2 h-4 w-4" />
Register
</Button>
</Link>
</>
)}
</nav>
<div className="flex items-center gap-2">
<nav className="flex items-center gap-1 sm:gap-2">
<Link href="/" passHref>
<Button variant={pathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
Browse Toys
</Button>
</Link>
{isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full h-9 w-9 md:h-10 md:w-10">
<UserCircle2 className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/profile">
<UserCircle2 className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogIn className="mr-2 h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<>
<Link href="/login" passHref>
<Button variant={pathname === '/login' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
<LogIn className="mr-2 h-4 w-4" />
Login
</Button>
</Link>
<Link href="/register" passHref>
<Button variant="default" size="sm" className="px-2 sm:px-3">
<UserPlus className="mr-2 h-4 w-4" />
Register
</Button>
</Link>
</>
)}
</nav>
<ThemeToggleButton />
</div>
</div>
</header>
);

View File

@ -0,0 +1,9 @@
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import type { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -0,0 +1,50 @@
'use client';
import * as React from 'react';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
export function ThemeToggleButton() {
const { setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
// Render a placeholder or null to avoid hydration mismatch, matching size of actual button
return <Button variant="outline" size="icon" disabled className="h-9 w-9 md:h-10 md:w-10 opacity-0 pointer-events-none"><Sun className="h-[1.2rem] w-[1.2rem]" /></Button>;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}