please add dark and light theme switch
This commit is contained in:
parent
606d2adb96
commit
128334e790
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
@ -35,6 +36,12 @@ export default function RootLayout({
|
|||
<body
|
||||
className={cn('min-h-screen bg-background font-body antialiased flex flex-col', ptSans.variable)}
|
||||
suppressHydrationWarning={true}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Header />
|
||||
<main className="flex-grow container mx-auto px-4 py-8">
|
||||
|
|
@ -42,6 +49,7 @@ export default function RootLayout({
|
|||
</main>
|
||||
<Footer />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,16 +38,18 @@ 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">
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -80,13 +78,13 @@ export default function Header() {
|
|||
) : (
|
||||
<>
|
||||
<Link href="/login" passHref>
|
||||
<Button variant={pathname === '/login' ? 'secondary' : 'ghost'} size="sm">
|
||||
<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">
|
||||
<Button variant="default" size="sm" className="px-2 sm:px-3">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Register
|
||||
</Button>
|
||||
|
|
@ -94,6 +92,8 @@ export default function Header() {
|
|||
</>
|
||||
)}
|
||||
</nav>
|
||||
<ThemeToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue