From 620b5d0012a391f22f1f5937a0822a873d304b5f Mon Sep 17 00:00:00 2001 From: Mohammed Rayan A Date: Thu, 16 Jul 2026 00:08:56 +0530 Subject: [PATCH] feat: add light/dark/system theme toggle to navbar - Upgrade ThemeContext to support 'system' mode with live OS preference listener - Add ThemeToggle component (pill variant for navbars, row variant for sidebars) - Add LegalNav shared sticky header with ThemeToggle for /privacy and /terms - Update Layout.tsx sidebar (row) and mobile header (pill) to use ThemeToggle - Persist theme mode to localStorage as 'theme-mode' key --- app/components/Layout.tsx | 17 ++----- app/components/ui/LegalNav.tsx | 29 ++++++++++++ app/components/ui/ThemeToggle.tsx | 78 +++++++++++++++++++++++++++++++ app/contexts/ThemeContext.tsx | 62 ++++++++++++++++++------ app/privacy/page.tsx | 21 ++------- app/terms/page.tsx | 21 ++------- 6 files changed, 165 insertions(+), 63 deletions(-) create mode 100644 app/components/ui/LegalNav.tsx create mode 100644 app/components/ui/ThemeToggle.tsx diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index c9c62d2..3a1b374 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -1,9 +1,9 @@ 'use client' -import { Bot, LayoutDashboard, LogOut, Moon, Plus, Share2, Sun, User } from 'lucide-react' +import { Bot, LayoutDashboard, LogOut, Plus, Share2, User } from 'lucide-react' import { ReactNode } from 'react' import { useAuth } from '../contexts/AuthContext' -import { useTheme } from '../contexts/ThemeContext' +import { ThemeToggle } from './ui/ThemeToggle' type Page = 'dashboard' | 'add' | 'shared' | 'ai' | 'profile' @@ -23,7 +23,6 @@ const navItems = [ export function Layout({ children, currentPage, onNavigate }: LayoutProps) { const { signOut } = useAuth() - const { theme, toggleTheme } = useTheme() return (
@@ -68,13 +67,7 @@ export function Layout({ children, currentPage, onNavigate }: LayoutProps) {
- +
- + diff --git a/app/components/ui/LegalNav.tsx b/app/components/ui/LegalNav.tsx new file mode 100644 index 0000000..e9a40a7 --- /dev/null +++ b/app/components/ui/LegalNav.tsx @@ -0,0 +1,29 @@ +'use client' + +import { ArrowLeft } from 'lucide-react' +import Link from 'next/link' +import { ThemeToggle } from './ThemeToggle' + +export function LegalNav() { + return ( +
+
+ + DumpIt + DumpIt + +
+ + + + Back to Home + Home + +
+
+
+ ) +} diff --git a/app/components/ui/ThemeToggle.tsx b/app/components/ui/ThemeToggle.tsx new file mode 100644 index 0000000..afe44ab --- /dev/null +++ b/app/components/ui/ThemeToggle.tsx @@ -0,0 +1,78 @@ +'use client' + +import { Monitor, Moon, Sun } from 'lucide-react' +import { useTheme, type ThemeMode } from '../../contexts/ThemeContext' + +const options: { mode: ThemeMode; Icon: typeof Sun; label: string }[] = [ + { mode: 'light', Icon: Sun, label: 'Light mode' }, + { mode: 'system', Icon: Monitor, label: 'System mode' }, + { mode: 'dark', Icon: Moon, label: 'Dark mode' }, +] + +interface ThemeToggleProps { + /** 'pill' = compact 3-icon pill (default for navbars). 'row' = full-width row items for sidebars. */ + variant?: 'pill' | 'row' + className?: string +} + +export function ThemeToggle({ variant = 'pill', className = '' }: ThemeToggleProps) { + const { themeMode, setThemeMode } = useTheme() + + if (variant === 'row') { + return ( +
+ {options.map(({ mode, Icon, label }) => { + const active = themeMode === mode + return ( + + ) + })} +
+ ) + } + + // Pill variant + return ( +
+ {options.map(({ mode, Icon, label }) => { + const active = themeMode === mode + return ( + + ) + })} +
+ ) +} diff --git a/app/contexts/ThemeContext.tsx b/app/contexts/ThemeContext.tsx index 54aad7e..878014d 100644 --- a/app/contexts/ThemeContext.tsx +++ b/app/contexts/ThemeContext.tsx @@ -2,36 +2,70 @@ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; -type Theme = 'light' | 'dark'; +export type ThemeMode = 'light' | 'dark' | 'system'; interface ThemeContextType { - theme: Theme; + /** The currently active rendered theme (always 'light' or 'dark') */ + resolvedTheme: 'light' | 'dark'; + /** The user's saved preference, including 'system' */ + themeMode: ThemeMode; + setThemeMode: (mode: ThemeMode) => void; + /** Legacy: kept for backwards compatibility with components that used toggleTheme */ toggleTheme: () => void; } const ThemeContext = createContext(undefined); +function getSystemTheme(): 'light' | 'dark' { + if (typeof window === 'undefined') return 'light'; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function applyTheme(resolved: 'light' | 'dark') { + document.documentElement.classList.toggle('dark', resolved === 'dark'); +} + export function ThemeProvider({ children }: { children: ReactNode }) { - const [theme, setTheme] = useState('light'); + const [themeMode, setThemeModeState] = useState('system'); + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light'); + // On mount: read saved preference, apply it useEffect(() => { - // Check for saved theme preference or system preference - const savedTheme = localStorage.getItem('theme') as Theme; - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - const initialTheme = savedTheme || systemTheme; - setTheme(initialTheme); - document.documentElement.classList.toggle('dark', initialTheme === 'dark'); + const saved = (localStorage.getItem('theme-mode') as ThemeMode) || 'system'; + const resolved = saved === 'system' ? getSystemTheme() : saved; + setThemeModeState(saved); + setResolvedTheme(resolved); + applyTheme(resolved); }, []); + // Listen for system preference changes when in 'system' mode + useEffect(() => { + if (themeMode !== 'system') return; + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const handler = (e: MediaQueryListEvent) => { + const resolved = e.matches ? 'dark' : 'light'; + setResolvedTheme(resolved); + applyTheme(resolved); + }; + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [themeMode]); + + const setThemeMode = (mode: ThemeMode) => { + const resolved = mode === 'system' ? getSystemTheme() : mode; + setThemeModeState(mode); + setResolvedTheme(resolved); + localStorage.setItem('theme-mode', mode); + applyTheme(resolved); + }; + + // Legacy compat: toggle between light and dark (skips system) const toggleTheme = () => { - const newTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(newTheme); - localStorage.setItem('theme', newTheme); - document.documentElement.classList.toggle('dark', newTheme === 'dark'); + setThemeMode(resolvedTheme === 'light' ? 'dark' : 'light'); }; return ( - + {children} ); diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index 44f03b1..c4cec1c 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import { ArrowLeft, Shield, Lock, Eye, FileText, Database, ShieldAlert } from 'lucide-react' -import Link from 'next/link' +import { Shield, Lock, Eye, FileText, Database, ShieldAlert } from 'lucide-react' +import { LegalNav } from '../components/ui/LegalNav' import Footer from '../components/landing/Footer' export const metadata: Metadata = { @@ -82,22 +82,7 @@ const sections = [ export default function PrivacyPolicy() { return (
- {/* Sticky Header */} -
-
- - DumpIt - DumpIt - - - - Back to Home - -
-
+ {/* Hero */}
diff --git a/app/terms/page.tsx b/app/terms/page.tsx index c06f409..9a1c905 100644 --- a/app/terms/page.tsx +++ b/app/terms/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import { ArrowLeft, Scale, ShieldAlert, Key, UserCheck, AlertTriangle, HelpCircle } from 'lucide-react' -import Link from 'next/link' +import { Scale, ShieldAlert, Key, UserCheck, AlertTriangle, HelpCircle } from 'lucide-react' +import { LegalNav } from '../components/ui/LegalNav' import Footer from '../components/landing/Footer' export const metadata: Metadata = { @@ -79,22 +79,7 @@ const sections = [ export default function TermsOfService() { return (
- {/* Sticky Header */} -
-
- - DumpIt - DumpIt - - - - Back to Home - -
-
+ {/* Hero */}