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", "genkit": "^1.8.0",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"next": "15.3.3", "next": "15.3.3",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@ -43,6 +43,7 @@
"genkit": "^1.8.0", "genkit": "^1.8.0",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"next": "15.3.3", "next": "15.3.3",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.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 Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer'; import Footer from '@/components/layout/Footer';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/theme-provider';
const ptSans = PT_Sans({ const ptSans = PT_Sans({
subsets: ['latin'], 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)} className={cn('min-h-screen bg-background font-body antialiased flex flex-col', ptSans.variable)}
suppressHydrationWarning={true} suppressHydrationWarning={true}
> >
<Header /> <ThemeProvider
<main className="flex-grow container mx-auto px-4 py-8"> attribute="class"
{children} defaultTheme="system"
</main> enableSystem
<Footer /> disableTransitionOnChange
<Toaster /> >
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
<Toaster />
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

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