please add dark and light theme switch
This commit is contained in:
parent
606d2adb96
commit
128334e790
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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