mirror of
https://github.com/nicetry247/offlineacademy.git
synced 2026-08-27 10:56:48 +00:00
initial commit
This commit is contained in:
Executable
+131
@@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
||||
import { Sun, Moon, Monitor, ChevronDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
resolvedTheme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>('system')
|
||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark')
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Initialize from localStorage
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('theme') as Theme | null
|
||||
if (stored) {
|
||||
setTheme(stored)
|
||||
}
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
const root = document.documentElement
|
||||
let resolved: 'light' | 'dark'
|
||||
|
||||
if (theme === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
} else {
|
||||
resolved = theme
|
||||
}
|
||||
|
||||
setResolvedTheme(resolved)
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(resolved)
|
||||
root.style.colorScheme = resolved
|
||||
localStorage.setItem('theme', theme)
|
||||
}, [theme, mounted])
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
if (!mounted || theme !== 'system') return
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = () => {
|
||||
const resolved = mediaQuery.matches ? 'dark' : 'light'
|
||||
setResolvedTheme(resolved)
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(resolved)
|
||||
document.documentElement.style.colorScheme = resolved
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [theme, mounted])
|
||||
|
||||
const setThemeCallback = useCallback((newTheme: Theme) => {
|
||||
setTheme(newTheme)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme: 'system', setTheme: () => {}, resolvedTheme: 'dark' }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme: setThemeCallback, resolvedTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme()
|
||||
|
||||
const themes: { value: Theme; label: string; icon: React.ReactNode }[] = [
|
||||
{ value: 'light', label: 'Light', icon: <Sun className="h-4 w-4" /> },
|
||||
{ value: 'dark', label: 'Dark', icon: <Moon className="h-4 w-4" /> },
|
||||
{ value: 'system', label: 'System', icon: <Monitor className="h-4 w-4" /> },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative"
|
||||
onClick={() => {
|
||||
const currentIndex = themes.findIndex(t => t.value === theme)
|
||||
const nextIndex = (currentIndex + 1) % themes.length
|
||||
setTheme(themes[nextIndex].value)
|
||||
}}
|
||||
aria-label={`Current theme: ${theme}. Click to cycle.`}
|
||||
title={`Theme: ${theme.charAt(0).toUpperCase() + theme.slice(1)}`}
|
||||
>
|
||||
{themes.find(t => t.value === resolvedTheme)?.icon || <Monitor className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 opacity-0 invisible transition-all group-hover:opacity-100 group-hover:visible pointer-events-none">
|
||||
<div className="bg-muted px-2 py-1 rounded text-xs text-muted-foreground whitespace-nowrap">
|
||||
Theme: {theme.charAt(0).toUpperCase() + theme.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user