Add dark mode toggle via next-themes

Uses next-themes with system default. Toggle button in sidebar
switches between light/dark and persists to localStorage.
CSS variables for dark mode were already defined in globals.css.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jerick
2026-05-25 18:10:22 +00:00
parent 4a0f036d01
commit 4809edf73a
4 changed files with 28 additions and 3 deletions

View File

@@ -18,6 +18,7 @@
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"next": "16.2.4", "next": "16.2.4",
"next-auth": "^5.0.0-beta.31", "next-auth": "^5.0.0-beta.31",
"next-themes": "^0.4.6",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"pg": "^8.20.0", "pg": "^8.20.0",
"react": "19.2.4", "react": "19.2.4",

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/layout/ThemeProvider";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -27,7 +28,9 @@ export default function RootLayout({
lang="en" lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col">
<ThemeProvider>{children}</ThemeProvider>
</body>
</html> </html>
); );
} }

View File

@@ -3,10 +3,11 @@
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { signOut } from 'next-auth/react' import { signOut } from 'next-auth/react'
import { useTheme } from 'next-themes'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { import {
LayoutDashboard, CreditCard, ArrowLeftRight, LayoutDashboard, CreditCard, ArrowLeftRight,
Upload, PiggyBank, TrendingUp, LogOut, Upload, PiggyBank, TrendingUp, LogOut, Sun, Moon,
} from 'lucide-react' } from 'lucide-react'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
@@ -21,6 +22,7 @@ const navItems = [
export function Sidebar() { export function Sidebar() {
const pathname = usePathname() const pathname = usePathname()
const { resolvedTheme, setTheme } = useTheme()
return ( return (
<aside className="flex w-56 shrink-0 flex-col border-r bg-card"> <aside className="flex w-56 shrink-0 flex-col border-r bg-card">
@@ -46,7 +48,15 @@ export function Sidebar() {
))} ))}
</nav> </nav>
<Separator /> <Separator />
<div className="p-2"> <div className="p-2 space-y-1">
<button
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
{resolvedTheme === 'dark'
? <><Sun className="h-4 w-4 shrink-0" />Light mode</>
: <><Moon className="h-4 w-4 shrink-0" />Dark mode</>}
</button>
<button <button
onClick={() => signOut({ callbackUrl: '/login' })} onClick={() => signOut({ callbackUrl: '/login' })}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"

View File

@@ -0,0 +1,11 @@
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</NextThemesProvider>
)
}