Backend: .NET 10 Clean Architecture + LiteCqrs.Net + EF Core/PostgreSQL + Identity/JWT. Frontend: React 19 + Vite + TanStack Query/Router + Tailwind v4 with a retro CRT theme. Docker/compose deployment mirroring PnvPanel's conventions, scoped down to the current base feature set.
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
|
|
|
export type Theme = 'light' | 'dark' | 'system'
|
|
|
|
type ThemeContextValue = {
|
|
theme: Theme
|
|
setTheme: (theme: Theme) => void
|
|
}
|
|
|
|
const STORAGE_KEY = 'tw-theme'
|
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
|
|
|
|
function resolve(theme: Theme): 'light' | 'dark' {
|
|
if (theme === 'system') {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
}
|
|
return theme
|
|
}
|
|
|
|
function applyTheme(theme: Theme) {
|
|
const root = document.documentElement
|
|
root.classList.toggle('dark', resolve(theme) === 'dark')
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>(
|
|
() => (localStorage.getItem(STORAGE_KEY) as Theme | null) ?? 'dark',
|
|
)
|
|
|
|
useEffect(() => {
|
|
applyTheme(theme)
|
|
if (theme !== 'system') return
|
|
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
const onChange = () => applyTheme('system')
|
|
media.addEventListener('change', onChange)
|
|
return () => media.removeEventListener('change', onChange)
|
|
}, [theme])
|
|
|
|
const setTheme = (next: Theme) => {
|
|
localStorage.setItem(STORAGE_KEY, next)
|
|
setThemeState(next)
|
|
}
|
|
|
|
return <ThemeContext value={{ theme, setTheme }}>{children}</ThemeContext>
|
|
}
|
|
|
|
export function useTheme(): ThemeContextValue {
|
|
const ctx = useContext(ThemeContext)
|
|
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
|
return ctx
|
|
}
|