From ce3c6984c4c92be3f20f65908a32d6c125c7aeb4 Mon Sep 17 00:00:00 2001 From: Sagar Sunil Bhedodkar Date: Tue, 21 Oct 2025 12:23:10 +0530 Subject: [PATCH 1/2] refactor: use Appearance enum and centralize labels/icons --- resources/js/components/appearance-tabs.tsx | 46 ++++++++++----------- resources/js/hooks/use-appearance.tsx | 35 +++++++++++++--- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/resources/js/components/appearance-tabs.tsx b/resources/js/components/appearance-tabs.tsx index 1a1e271f2..bf16c25a6 100644 --- a/resources/js/components/appearance-tabs.tsx +++ b/resources/js/components/appearance-tabs.tsx @@ -1,6 +1,5 @@ -import { Appearance, useAppearance } from '@/hooks/use-appearance'; +import { Appearance, AppearanceLabels, AppearanceIcons, useAppearance } from '@/hooks/use-appearance'; import { cn } from '@/lib/utils'; -import { LucideIcon, Monitor, Moon, Sun } from 'lucide-react'; import { HTMLAttributes } from 'react'; export default function AppearanceToggleTab({ @@ -9,35 +8,34 @@ export default function AppearanceToggleTab({ }: HTMLAttributes) { const { appearance, updateAppearance } = useAppearance(); - const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [ - { value: 'light', icon: Sun, label: 'Light' }, - { value: 'dark', icon: Moon, label: 'Dark' }, - { value: 'system', icon: Monitor, label: 'System' }, - ]; - return (
- {tabs.map(({ value, icon: Icon, label }) => ( - - ))} + {Object.values(Appearance).map((value) => { + const Icon = AppearanceIcons[value]; + const label = AppearanceLabels[value]; + + return ( + + ); + })}
); } diff --git a/resources/js/hooks/use-appearance.tsx b/resources/js/hooks/use-appearance.tsx index 2c6b56829..18fe46ce9 100644 --- a/resources/js/hooks/use-appearance.tsx +++ b/resources/js/hooks/use-appearance.tsx @@ -1,6 +1,29 @@ import { useCallback, useEffect, useState } from 'react'; +import { Sun, Moon, Monitor } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +// --- Changed type from string union to enum --- +export enum Appearance { + LIGHT = 'light', + DARK = 'dark', + SYSTEM = 'system', +} + +// --- Centralized labels --- +export const AppearanceLabels: Record = { + [Appearance.LIGHT]: 'Light', + [Appearance.DARK]: 'Dark', + [Appearance.SYSTEM]: 'System', +}; + +// --- Centralized icons --- +export const AppearanceIcons: Record = { + [Appearance.LIGHT]: Sun, + [Appearance.DARK]: Moon, + [Appearance.SYSTEM]: Monitor, +}; -export type Appearance = 'light' | 'dark' | 'system'; +const APPEARANCE_KEY = 'appearance'; const prefersDark = () => { if (typeof window === 'undefined') { @@ -21,7 +44,7 @@ const setCookie = (name: string, value: string, days = 365) => { const applyTheme = (appearance: Appearance) => { const isDark = - appearance === 'dark' || (appearance === 'system' && prefersDark()); + appearance === Appearance.DARK || (appearance === Appearance.SYSTEM && prefersDark()); document.documentElement.classList.toggle('dark', isDark); document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; @@ -37,12 +60,12 @@ const mediaQuery = () => { const handleSystemThemeChange = () => { const currentAppearance = localStorage.getItem('appearance') as Appearance; - applyTheme(currentAppearance || 'system'); + applyTheme(currentAppearance || Appearance.SYSTEM); }; export function initializeTheme() { const savedAppearance = - (localStorage.getItem('appearance') as Appearance) || 'system'; + (localStorage.getItem('appearance') as Appearance) || Appearance.SYSTEM; applyTheme(savedAppearance); @@ -51,7 +74,7 @@ export function initializeTheme() { } export function useAppearance() { - const [appearance, setAppearance] = useState('system'); + const [appearance, setAppearance] = useState(Appearance.SYSTEM); const updateAppearance = useCallback((mode: Appearance) => { setAppearance(mode); @@ -71,7 +94,7 @@ export function useAppearance() { ) as Appearance | null; // eslint-disable-next-line react-hooks/set-state-in-effect - updateAppearance(savedAppearance || 'system'); + updateAppearance(savedAppearance || Appearance.SYSTEM); return () => mediaQuery()?.removeEventListener( From 88722054cd54a14da40d96179cfffdfafd5b7580 Mon Sep 17 00:00:00 2001 From: Sagar Sunil Bhedodkar Date: Tue, 21 Oct 2025 12:31:04 +0530 Subject: [PATCH 2/2] fix: use APPEARANCE_KEY variable to satisfy ESLint --- resources/js/hooks/use-appearance.tsx | 109 +++++++++++++------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/resources/js/hooks/use-appearance.tsx b/resources/js/hooks/use-appearance.tsx index 18fe46ce9..ad79124f5 100644 --- a/resources/js/hooks/use-appearance.tsx +++ b/resources/js/hooks/use-appearance.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { Sun, Moon, Monitor } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; -// --- Changed type from string union to enum --- +// --- Define enum for theme modes --- export enum Appearance { LIGHT = 'light', DARK = 'dark', @@ -23,85 +23,86 @@ export const AppearanceIcons: Record = { [Appearance.SYSTEM]: Monitor, }; +// --- Key for localStorage & cookies --- const APPEARANCE_KEY = 'appearance'; -const prefersDark = () => { - if (typeof window === 'undefined') { - return false; - } - - return window.matchMedia('(prefers-color-scheme: dark)').matches; -}; +// Detect OS-level dark mode +const prefersDark = (): boolean => + typeof window !== 'undefined' && + window.matchMedia('(prefers-color-scheme: dark)').matches; +// Set cookie (SSR-friendly) const setCookie = (name: string, value: string, days = 365) => { - if (typeof document === 'undefined') { - return; - } - + if (typeof document === 'undefined') return; const maxAge = days * 24 * 60 * 60; document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; }; +// Apply theme to const applyTheme = (appearance: Appearance) => { const isDark = - appearance === Appearance.DARK || (appearance === Appearance.SYSTEM && prefersDark()); - + appearance === Appearance.DARK || + (appearance === Appearance.SYSTEM && prefersDark()); + if (typeof document === 'undefined') return; document.documentElement.classList.toggle('dark', isDark); document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; }; -const mediaQuery = () => { - if (typeof window === 'undefined') { - return null; - } - - return window.matchMedia('(prefers-color-scheme: dark)'); -}; - -const handleSystemThemeChange = () => { - const currentAppearance = localStorage.getItem('appearance') as Appearance; - applyTheme(currentAppearance || Appearance.SYSTEM); -}; +// Listen to system theme changes +const getMediaQuery = (): MediaQueryList | null => + typeof window === 'undefined' + ? null + : window.matchMedia('(prefers-color-scheme: dark)'); export function initializeTheme() { - const savedAppearance = - (localStorage.getItem('appearance') as Appearance) || Appearance.SYSTEM; - - applyTheme(savedAppearance); - - // Add the event listener for system theme changes... - mediaQuery()?.addEventListener('change', handleSystemThemeChange); + const saved = + (typeof localStorage !== 'undefined' + ? (localStorage.getItem(APPEARANCE_KEY) as Appearance | null) + : null) || Appearance.SYSTEM; + + applyTheme(saved); + + const mq = getMediaQuery(); + if (mq) { + const listener = () => { + const current = + (localStorage.getItem(APPEARANCE_KEY) as Appearance) || + Appearance.SYSTEM; + applyTheme(current); + }; + mq.addEventListener('change', listener); + } } export function useAppearance() { - const [appearance, setAppearance] = useState(Appearance.SYSTEM); + const getInitial = (): Appearance => { + if (typeof window === 'undefined') return Appearance.SYSTEM; + return ( + (localStorage.getItem(APPEARANCE_KEY) as Appearance) || + Appearance.SYSTEM + ); + }; + + const [appearance, setAppearance] = useState(getInitial); const updateAppearance = useCallback((mode: Appearance) => { setAppearance(mode); - - // Store in localStorage for client-side persistence... - localStorage.setItem('appearance', mode); - - // Store in cookie for SSR... - setCookie('appearance', mode); - + localStorage.setItem(APPEARANCE_KEY, mode); + setCookie(APPEARANCE_KEY, mode); applyTheme(mode); }, []); useEffect(() => { - const savedAppearance = localStorage.getItem( - 'appearance', - ) as Appearance | null; - - // eslint-disable-next-line react-hooks/set-state-in-effect - updateAppearance(savedAppearance || Appearance.SYSTEM); - - return () => - mediaQuery()?.removeEventListener( - 'change', - handleSystemThemeChange, - ); - }, [updateAppearance]); + const mq = getMediaQuery(); + const handleChange = () => { + const current = + (localStorage.getItem(APPEARANCE_KEY) as Appearance) || + Appearance.SYSTEM; + applyTheme(current); + }; + mq?.addEventListener('change', handleChange); + return () => mq?.removeEventListener('change', handleChange); + }, []); return { appearance, updateAppearance } as const; }