10 changed files with 89 additions and 150 deletions
@ -1,39 +1,44 @@ |
|||
import { useTheme } from "@app/core/hooks/useTheme"; |
|||
import { Moon, Sun } from "lucide-react"; |
|||
import React from "react"; |
|||
import { cn } from "@app/core/utils/cn"; |
|||
import { Monitor, Moon, Sun } from "lucide-react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
type ThemePreference = "light" | "dark" | "system"; |
|||
|
|||
export default function ThemeSwitcher({ |
|||
className = "", |
|||
}: { className?: string }) { |
|||
const currentTheme = useTheme(); // Get current theme from DOM
|
|||
const [theme, setTheme] = React.useState<Theme>(currentTheme); |
|||
|
|||
React.useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
localStorage.setItem("theme", theme); |
|||
}, [theme]); |
|||
}: { |
|||
className?: string; |
|||
}) { |
|||
const { theme, preference, setPreference } = useTheme(); |
|||
|
|||
const themeIcons = { |
|||
light: ( |
|||
<Sun className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
dark: ( |
|||
<Moon className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
light: <Sun className="size-5" />, |
|||
dark: <Moon className="size-5" />, |
|||
system: <Monitor className="size-5" />, |
|||
}; |
|||
|
|||
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light"); |
|||
const toggleTheme = () => { |
|||
const preferences: ThemePreference[] = ["light", "dark", "system"]; |
|||
const currentIndex = preferences.indexOf(preference); |
|||
const nextPreference = preferences[(currentIndex + 1) % preferences.length]; |
|||
setPreference(nextPreference); |
|||
}; |
|||
|
|||
return ( |
|||
<button |
|||
type="button" |
|||
className={`transition-all duration-300 hover:text-accent ${className}`} |
|||
className={cn( |
|||
"transition-all duration-300 scale-100 cursor-pointer m-6 p-2", |
|||
className, |
|||
)} |
|||
onClick={toggleTheme} |
|||
aria-label={`Current theme: ${theme}. Click to change theme.`} |
|||
aria-label={ |
|||
preference === "system" |
|||
? `System theme (currently ${theme}). Click to change theme.` |
|||
: `Current theme: ${theme}. Click to change theme.` |
|||
} |
|||
> |
|||
{themeIcons[theme]} |
|||
{themeIcons[preference]} |
|||
</button> |
|||
); |
|||
} |
|||
|
|||
@ -1,67 +0,0 @@ |
|||
import type React from "react"; |
|||
import { createContext, useContext, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark" | "system"; |
|||
|
|||
interface ThemeContextType { |
|||
theme: Theme; |
|||
setTheme: (theme: Theme) => void; |
|||
} |
|||
|
|||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined); |
|||
|
|||
export function ThemeProvider({ children }: { children: React.ReactNode }) { |
|||
const [theme, setTheme] = useState<Theme>(() => { |
|||
if (typeof window !== "undefined") { |
|||
const savedTheme = localStorage.getItem("theme") as Theme; |
|||
return savedTheme || "system"; |
|||
} |
|||
return "system"; |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const root = window.document.documentElement; |
|||
root.classList.remove("light", "dark"); |
|||
|
|||
if (theme === "system") { |
|||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") |
|||
.matches |
|||
? "dark" |
|||
: "light"; |
|||
root.classList.add(systemTheme); |
|||
} else { |
|||
root.classList.add(theme); |
|||
} |
|||
|
|||
localStorage.setItem("theme", theme); |
|||
}, [theme]); |
|||
|
|||
useEffect(() => { |
|||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); |
|||
|
|||
const handleChange = () => { |
|||
if (theme === "system") { |
|||
const root = window.document.documentElement; |
|||
root.classList.remove("light", "dark"); |
|||
root.classList.add(mediaQuery.matches ? "dark" : "light"); |
|||
} |
|||
}; |
|||
|
|||
mediaQuery.addEventListener("change", handleChange); |
|||
return () => mediaQuery.removeEventListener("change", handleChange); |
|||
}, [theme]); |
|||
|
|||
return ( |
|||
<ThemeContext.Provider value={{ theme, setTheme }}> |
|||
{children} |
|||
</ThemeContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export function useTheme() { |
|||
const context = useContext(ThemeContext); |
|||
if (context === undefined) { |
|||
throw new Error("useTheme must be used within a ThemeProvider"); |
|||
} |
|||
return context; |
|||
} |
|||
@ -1,37 +1,42 @@ |
|||
import { useEffect, useState } from "react"; |
|||
import { useCallback, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
type Theme = "light" | "dark" | "system"; |
|||
|
|||
export function useTheme() { |
|||
const [theme, setTheme] = useState<Theme>(() => { |
|||
if (typeof window === "undefined") return "light"; |
|||
return ( |
|||
(document.documentElement.getAttribute("data-theme") as Theme) || "light" |
|||
); |
|||
}); |
|||
const getSystemTheme = () => |
|||
window.matchMedia("(prefers-color-scheme: dark)").matches |
|||
? "dark" |
|||
: "light"; |
|||
|
|||
const getStoredPreference = useCallback( |
|||
(): Theme => (localStorage.getItem("theme") as Theme) || "system", |
|||
[], |
|||
); |
|||
|
|||
const [preference, setPreference] = useState<Theme>(() => |
|||
typeof window !== "undefined" ? getStoredPreference() : "light", |
|||
); |
|||
|
|||
const theme = preference === "system" ? getSystemTheme() : preference; |
|||
|
|||
useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
}, [theme]); |
|||
|
|||
useEffect(() => { |
|||
const observer = new MutationObserver((mutations) => { |
|||
for (const mutation of mutations) { |
|||
if ( |
|||
mutation.type === "attributes" && |
|||
mutation.attributeName === "data-theme" |
|||
) { |
|||
const newTheme = document.documentElement.getAttribute( |
|||
"data-theme", |
|||
) as Theme; |
|||
setTheme(newTheme); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
observer.observe(document.documentElement, { |
|||
attributes: true, |
|||
attributeFilter: ["data-theme"], |
|||
}); |
|||
|
|||
return () => observer.disconnect(); |
|||
}, []); |
|||
|
|||
return theme; |
|||
if (preference !== "system") return; |
|||
|
|||
const media = window.matchMedia("(prefers-color-scheme: dark)"); |
|||
const updateTheme = () => setPreference(getStoredPreference()); |
|||
|
|||
media.addEventListener("change", updateTheme); |
|||
return () => media.removeEventListener("change", updateTheme); |
|||
}, [preference, getStoredPreference]); |
|||
|
|||
const setPreferenceValue = (newPreference: Theme) => { |
|||
localStorage.setItem("theme", newPreference); |
|||
setPreference(newPreference); |
|||
}; |
|||
|
|||
return { theme, preference, setPreference: setPreferenceValue }; |
|||
} |
|||
|
|||
Loading…
Reference in new issue