mirror of
https://github.com/R0m1k3/Reviflow.git
synced 2026-10-13 18:27:28 +02:00
feat: implement dark mode with theme toggle and persistence
This commit is contained in:
1 parent
7a816c8230
commit
e7e5382de4
7 files changed
+163
-69
No files matched your search
@@ -14,6 +14,7 @@ import { ParentalGate } from './features/auth/ParentalGate';
|
||||
import { ProfileSelectionPage } from './features/auth/ProfileSelectionPage';
|
||||
import { ParentHubPage } from './pages/ParentHubPage';
|
||||
import { HistoryPage } from './features/quiz/HistoryPage';
|
||||
import { useThemeEffect } from './stores/useTheme';
|
||||
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
@@ -74,6 +75,8 @@ function ParentalProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
function App() {
|
||||
useThemeEffect();
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTheme } from '../stores/useTheme';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { isDark, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-bold transition-all ${isDark
|
||||
? 'bg-slate-800 text-yellow-400 border-slate-700 hover:bg-slate-700'
|
||||
: 'bg-white text-indigo-900 border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
title={isDark ? "Passer en mode clair" : "Passer en mode sombre"}
|
||||
aria-label={isDark ? "Passer en mode clair" : "Passer en mode sombre"}
|
||||
>
|
||||
{isDark ? <Moon className="w-4 h-4 fill-current" /> : <Sun className="w-4 h-4 fill-current" />}
|
||||
<span className="hidden sm:inline">{isDark ? 'Sombre' : 'Clair'}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+20
-4
@@ -11,16 +11,32 @@
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Theme Tokens */
|
||||
--bg-primary: #FDFCF8;
|
||||
--bg-secondary: #F8FAFC;
|
||||
--bg-card: #FFFFFF;
|
||||
--text-primary: #1E293B;
|
||||
--text-secondary: #64748B;
|
||||
--border-color: #E2E8F0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--bg-primary: #0F172A; /* slate-900 */
|
||||
--bg-secondary: #020617; /* slate-950 */
|
||||
--bg-card: #1E293B; /* slate-800 */
|
||||
--text-primary: #F1F5F9; /* slate-100 */
|
||||
--text-secondary: #94A3B8; /* slate-400 */
|
||||
--border-color: #334155; /* slate-700 */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: #FDFCF8;
|
||||
/* cream */
|
||||
color: #1E293B;
|
||||
/* slate-800 */
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { RecentRevisionsWidget } from '../features/dashboard/RecentRevisionsWidg
|
||||
import { LogOut, Flame } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
|
||||
// --- Avatar Configuration (Original Fun-Emoji Style) ---
|
||||
const AVATAR_SEEDS = [
|
||||
@@ -347,7 +348,7 @@ export default function DashboardPage() {
|
||||
const currentAvatarUrl = activeLearner?.avatar_url;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f8fafc] flex flex-col font-sans text-gray-900 overflow-x-hidden">
|
||||
<div className="min-h-screen bg-[#f8fafc] dark:bg-slate-900 flex flex-col font-sans text-gray-900 dark:text-gray-100 transition-colors duration-300 overflow-x-hidden">
|
||||
<AvatarSelector isOpen={isAvatarPickerOpen} onClose={() => setIsAvatarPickerOpen(false)} />
|
||||
<BadgeGalleryModal
|
||||
isOpen={isBadgeGalleryOpen}
|
||||
@@ -355,71 +356,76 @@ export default function DashboardPage() {
|
||||
unlockedBadges={unlockedBadgeCodes}
|
||||
/>
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-40">
|
||||
<nav className="bg-white/80 dark:bg-slate-900/80 backdrop-blur-md border-b border-gray-200 dark:border-slate-800 sticky top-0 z-40 transition-colors duration-300">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center gap-6">
|
||||
<Link to="/dashboard" className="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="Reviflow" className="w-8 h-8 rounded-lg shadow-sm object-cover bg-white" />
|
||||
<span className="text-lg font-bold tracking-tight text-gray-900">Reviflow</span>
|
||||
<span className="text-lg font-bold tracking-tight text-gray-900 dark:text-white">Reviflow</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden sm:flex items-center space-x-1">
|
||||
<Link to="/dashboard" className="px-3 py-2 text-gray-900 bg-gray-100/50 rounded-md text-sm font-medium">
|
||||
<Link to="/dashboard" className="px-3 py-2 text-gray-900 dark:text-white bg-gray-100/50 dark:bg-slate-800/50 rounded-md text-sm font-medium">
|
||||
Tableau de bord
|
||||
</Link>
|
||||
{user?.role === 'parent' && (
|
||||
<>
|
||||
<Link to="/parent-hub" className="px-3 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-50 rounded-md text-sm font-medium transition-colors">
|
||||
Espace Parent
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{user?.role === 'parent' && (
|
||||
<>
|
||||
<Link to="/parent-hub" className="px-3 py-2 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-slate-800 rounded-md text-sm font-medium transition-colors">
|
||||
Espace Parent
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Date/Time */}
|
||||
<div className="hidden lg:flex flex-col items-center justify-center">
|
||||
<span className="text-sm font-black text-gray-900 capitalize">
|
||||
{new Date().toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-indigo-500 bg-indigo-50 px-2 py-0.5 rounded-full">
|
||||
{new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Center: Date/Time */}
|
||||
<div className="hidden lg:flex flex-col items-center justify-center">
|
||||
<span className="text-sm font-black text-gray-900 dark:text-gray-100 capitalize">
|
||||
{new Date().toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-indigo-500 bg-indigo-50 dark:bg-indigo-900/30 dark:text-indigo-400 px-2 py-0.5 rounded-full">
|
||||
{new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Dyslexia Toggle */}
|
||||
<DyslexiaToggle />
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
<div className="h-6 w-px bg-gray-200 mx-1"></div>
|
||||
{/* Dyslexia Toggle */}
|
||||
<DyslexiaToggle />
|
||||
|
||||
{/* Profile Info */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden md:block text-right">
|
||||
<p className="text-sm font-bold text-gray-900 leading-none">
|
||||
{activeLearner?.first_name || user?.first_name}
|
||||
</p>
|
||||
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
||||
|
||||
{/* Profile Info */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden md:block text-right">
|
||||
<p className="text-sm font-bold text-gray-900 dark:text-white leading-none">
|
||||
{activeLearner?.first_name || user?.first_name}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => user?.role === 'learner' && setIsAvatarPickerOpen(true)}
|
||||
className="outline-none"
|
||||
disabled={user?.role !== 'learner'}
|
||||
>
|
||||
<AvatarDisplay url={currentAvatarUrl || null} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => user?.role === 'learner' && setIsAvatarPickerOpen(true)}
|
||||
className="outline-none"
|
||||
disabled={user?.role !== 'learner'}
|
||||
onClick={logout}
|
||||
className="p-2 text-gray-400 hover:text-red-900 transition-colors"
|
||||
title="Se déconnecter"
|
||||
>
|
||||
<AvatarDisplay url={currentAvatarUrl || null} />
|
||||
<LogOut className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 text-gray-400 hover:text-red-900 transition-colors"
|
||||
title="Se déconnecter"
|
||||
>
|
||||
<LogOut className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -431,7 +437,7 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{activeLearner && (activeLearner.streak_current || 0) > 0 && (
|
||||
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 bg-orange-50 border border-orange-100 rounded-full text-orange-700">
|
||||
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 bg-orange-50 dark:bg-orange-900/30 border border-orange-100 dark:border-orange-800 rounded-full text-orange-700 dark:text-orange-400">
|
||||
<Flame className="w-3 h-3 fill-orange-500 text-orange-500" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wide">
|
||||
{activeLearner.streak_current} jours
|
||||
@@ -439,10 +445,10 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 tracking-tight">
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
Bonjour {activeLearner?.first_name}.
|
||||
</h2>
|
||||
<p className="text-gray-500 mt-1">Prêt à avancer sur tes objectifs ?</p>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1">Prêt à avancer sur tes objectifs ?</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../lib/api';
|
||||
import { MasteryWidget } from '../features/dashboard/MasteryWidget';
|
||||
import { ActivityTimeline } from '../features/dashboard/ActivityTimeline';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
|
||||
export const ParentHubPage = () => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -70,9 +71,9 @@ export const ParentHubPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#FDFCF8] flex flex-col">
|
||||
<div className="min-h-screen bg-[#FDFCF8] dark:bg-slate-900 flex flex-col transition-colors duration-300">
|
||||
{/* Navigation */}
|
||||
<nav className="bg-white border-b border-gray-100 sticky top-0 z-40 shadow-sm/5">
|
||||
<nav className="bg-white dark:bg-slate-900 border-b border-gray-100 dark:border-slate-800 sticky top-0 z-40 shadow-sm/5">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
@@ -82,7 +83,7 @@ export const ParentHubPage = () => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xl font-black tracking-tight text-gray-900">Reviflow</span>
|
||||
<span className="text-xl font-black tracking-tight text-gray-900 dark:text-white">Reviflow</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
@@ -90,7 +91,7 @@ export const ParentHubPage = () => {
|
||||
<Users className="w-4 h-4" />
|
||||
Espace Parent
|
||||
</Link>
|
||||
<Link to="/settings" className="flex items-center gap-2 px-3 py-2 text-gray-500 hover:text-indigo-600 hover:bg-gray-50 rounded-lg text-sm font-medium transition-all">
|
||||
<Link to="/settings" className="flex items-center gap-2 px-3 py-2 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-slate-800 rounded-lg text-sm font-medium transition-all">
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
Paramètres
|
||||
</Link>
|
||||
@@ -98,9 +99,12 @@ export const ParentHubPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 pl-4 border-l border-gray-100">
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
<div className="flex items-center gap-3 pl-4 border-l border-gray-100 dark:border-gray-800">
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-gray-900 leading-none mb-1">{user?.first_name || 'Parent'}</p>
|
||||
<p className="text-sm font-bold text-gray-900 dark:text-white leading-none mb-1">{user?.first_name || 'Parent'}</p>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">Administrateur</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,50 +121,50 @@ export const ParentHubPage = () => {
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
|
||||
<header className="mb-10">
|
||||
<h1 className="text-4xl font-black text-slate-900 tracking-tight">Bonjour, {user?.first_name || 'Parent'} ! 👋</h1>
|
||||
<p className="text-slate-500 mt-2 text-lg font-medium">Suivez les progrès de vos apprentis en toute sérénité.</p>
|
||||
<h1 className="text-4xl font-black text-slate-900 dark:text-white tracking-tight">Bonjour, {user?.first_name || 'Parent'} ! 👋</h1>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-2 text-lg font-medium">Suivez les progrès de vos apprentis en toute sérénité.</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<div className="bg-white p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 border border-slate-50">
|
||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-50 dark:border-slate-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-4 bg-indigo-50 rounded-2xl text-indigo-600">
|
||||
<Users className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Apprentis</p>
|
||||
<p className="text-3xl font-black text-slate-900">{learnerProfiles.length}</p>
|
||||
<p className="text-3xl font-black text-slate-900 dark:text-white">{learnerProfiles.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 border border-slate-50">
|
||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-50 dark:border-slate-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-4 bg-amber-50 rounded-2xl text-amber-600">
|
||||
<TrendingUp className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Jetons AI</p>
|
||||
<p className="text-3xl font-black text-slate-900">{stats.total_tokens.toLocaleString()}</p>
|
||||
<p className="text-3xl font-black text-slate-900 dark:text-white">{stats.total_tokens.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 border border-slate-50">
|
||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-[2rem] shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-50 dark:border-slate-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-4 bg-emerald-50 rounded-2xl text-emerald-600">
|
||||
<div className="h-6 w-6 font-black flex items-center justify-center">€</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Coût estimé</p>
|
||||
<p className="text-3xl font-black text-slate-900">{stats.total_cost.toFixed(2)}</p>
|
||||
<p className="text-3xl font-black text-slate-900 dark:text-white">{stats.total_cost.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-black text-slate-900 tracking-tight border-b-4 border-indigo-100 pb-1">Mes Apprentis</h2>
|
||||
<h2 className="text-2xl font-black text-slate-900 dark:text-white tracking-tight border-b-4 border-indigo-100 dark:border-indigo-900 pb-1">Mes Apprentis</h2>
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="inline-flex items-center px-6 py-3 bg-indigo-600 text-white font-bold rounded-2xl hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 hover:-translate-y-1 active:scale-95"
|
||||
@@ -172,7 +176,7 @@ export const ParentHubPage = () => {
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{learnerProfiles.map(child => (
|
||||
<div key={child.id} className="bg-white rounded-[2.5rem] p-8 shadow-xl shadow-slate-200/50 border border-slate-50 hover:shadow-2xl hover:shadow-indigo-100 transition-all group">
|
||||
<div key={child.id} className="bg-white dark:bg-slate-800 rounded-[2.5rem] p-8 shadow-xl shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-50 dark:border-slate-700 hover:shadow-2xl hover:shadow-indigo-100 dark:hover:shadow-indigo-900/20 transition-all group">
|
||||
<div className="flex items-center space-x-5 mb-6">
|
||||
<div className="h-16 w-16 rounded-[1.5rem] bg-indigo-50 border-2 border-white shadow-md flex items-center justify-center text-2xl font-black text-indigo-400 overflow-hidden group-hover:scale-110 transition-transform">
|
||||
{child.avatar_url && child.avatar_url.startsWith('http') ? (
|
||||
@@ -182,11 +186,11 @@ export const ParentHubPage = () => {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-slate-900">{child.first_name}</h3>
|
||||
<h3 className="text-xl font-black text-slate-900 dark:text-white">{child.first_name}</h3>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Niveau {child.level || 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 pt-6 border-t border-slate-100">
|
||||
<div className="space-y-4 pt-6 border-t border-slate-100 dark:border-slate-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-slate-500">Série actuelle</span>
|
||||
<span className="px-3 py-1 bg-orange-50 text-orange-600 text-xs font-black rounded-full border border-orange-100">
|
||||
@@ -195,7 +199,7 @@ export const ParentHubPage = () => {
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500 font-medium">Expérience</span>
|
||||
<span className="font-bold text-slate-900">{child.xp || 0} XP</span>
|
||||
<span className="font-bold text-slate-900 dark:text-slate-200">{child.xp || 0} XP</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedChildId(child.id)}
|
||||
@@ -323,7 +327,7 @@ export const ParentHubPage = () => {
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="mt-auto py-12 border-t border-slate-100 bg-white">
|
||||
<footer className="mt-auto py-12 border-t border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||
<div className="max-w-7xl mx-auto px-8 flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-slate-200 rounded-lg"></div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { create } from 'zustand';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface ThemeState {
|
||||
isDark: boolean;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (isDark: boolean) => void;
|
||||
}
|
||||
|
||||
export const useTheme = create<ThemeState>((set) => {
|
||||
// 1. Check local storage or system preference on initialization
|
||||
const stored = localStorage.getItem('theme-mode');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
const initialIsDark = stored ? stored === 'dark' : systemPrefersDark;
|
||||
|
||||
return {
|
||||
isDark: initialIsDark,
|
||||
toggleTheme: () => set((state) => {
|
||||
const newIsDark = !state.isDark;
|
||||
localStorage.setItem('theme-mode', newIsDark ? 'dark' : 'light');
|
||||
return { isDark: newIsDark };
|
||||
}),
|
||||
setTheme: (isDark) => set(() => {
|
||||
localStorage.setItem('theme-mode', isDark ? 'dark' : 'light');
|
||||
return { isDark };
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// Hook to handle the side effect of applying the class to HTML/Body
|
||||
export function useThemeEffect() {
|
||||
const isDark = useTheme((state) => state.isDark);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
if (isDark) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}, [isDark]);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
|
||||
Reference in new issue
Block a user