Fix de la sauvegarde du RTE avec la possibilité de faire un localStorage
This commit is contained in:
@@ -16,6 +16,7 @@ export default function WritePage() {
|
||||
return (
|
||||
<RichTextEditor
|
||||
ref={editorRef}
|
||||
editorId={currentChapterId}
|
||||
initialContent={currentChapter?.content || ''}
|
||||
onSave={(html) => updateChapter(currentChapterId, { content: html })}
|
||||
onAiTransform={async (text, mode) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useMemo } from 'react';
|
||||
import {
|
||||
Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, List, Heading1, Heading2,
|
||||
Copy, Wand2, Check, RefreshCw, Maximize2, Loader2, MousePointerClick, History, RotateCcw,
|
||||
Copy, Wand2, Check, CheckCheck, RefreshCw, Maximize2, Loader2, MousePointerClick, History, RotateCcw,
|
||||
ChevronDown, ChevronUp, Layers
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface RichTextEditorHandle {
|
||||
}
|
||||
|
||||
interface RichTextEditorProps {
|
||||
editorId?: string; // Used to uniquely identify the draft in localStorage
|
||||
initialContent: string;
|
||||
onChange?: (html: string) => void;
|
||||
onSave?: (html: string) => void;
|
||||
@@ -34,13 +35,13 @@ interface VersionGroup {
|
||||
versions: Version[];
|
||||
}
|
||||
|
||||
const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({ initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref) => {
|
||||
const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({ editorId, initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Auto-Save State
|
||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved');
|
||||
const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db');
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Track sync state to avoid autosave loopbacks wiping current edits
|
||||
@@ -168,8 +169,21 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
useEffect(() => {
|
||||
if (!contentRef.current || initialContent === undefined) return;
|
||||
|
||||
let contentToLoad = initialContent;
|
||||
let hasLocalDraft = false;
|
||||
|
||||
// Check localStorage for a newer draft
|
||||
if (editorId) {
|
||||
const localDraft = localStorage.getItem(`draft_${editorId}`);
|
||||
if (localDraft && localDraft !== initialContent) {
|
||||
contentToLoad = localDraft;
|
||||
hasLocalDraft = true;
|
||||
setSaveStatus('saved_local');
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien
|
||||
if (initialContent === contentRef.current.innerHTML) return;
|
||||
if (contentToLoad === contentRef.current.innerHTML) return;
|
||||
|
||||
// 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si :
|
||||
// - L'éditeur est vide (premier chargement)
|
||||
@@ -177,12 +191,12 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
// - OU si l'utilisateur n'est PAS en train de focus l'éditeur
|
||||
const isUserEditing = document.activeElement === contentRef.current;
|
||||
|
||||
if (!isUserEditing || (contentRef.current.innerHTML === "" && initialContent !== "")) {
|
||||
contentRef.current.innerHTML = initialContent;
|
||||
syncRef.current = initialContent;
|
||||
latestContentRef.current = initialContent;
|
||||
if (!isUserEditing || (contentRef.current.innerHTML === "" && contentToLoad !== "")) {
|
||||
contentRef.current.innerHTML = contentToLoad;
|
||||
syncRef.current = contentToLoad;
|
||||
latestContentRef.current = contentToLoad;
|
||||
}
|
||||
}, [initialContent]);
|
||||
}, [initialContent, editorId]);
|
||||
|
||||
// Flush pending save on unmount
|
||||
useEffect(() => {
|
||||
@@ -213,9 +227,16 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
|
||||
if (onChange) onChange(currentHtml);
|
||||
|
||||
// Auto-Save Debounce
|
||||
if (onSave) {
|
||||
// 1. Save locally immediately
|
||||
if (editorId) {
|
||||
localStorage.setItem(`draft_${editorId}`, currentHtml);
|
||||
setSaveStatus('saved_local');
|
||||
} else {
|
||||
setSaveStatus('unsaved');
|
||||
}
|
||||
|
||||
// 2. Auto-Save Debounce for DB
|
||||
if (onSave) {
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
||||
|
||||
saveTimeoutRef.current = setTimeout(async () => {
|
||||
@@ -227,10 +248,15 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
syncRef.current = htmlToSave;
|
||||
try {
|
||||
await onSave(htmlToSave);
|
||||
setSaveStatus('saved_db');
|
||||
if (editorId) {
|
||||
// Once saved to DB, we can consider the local draft synced if we want,
|
||||
// or just keep it there. It will be overwritten on next load.
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auto-save failed:', err);
|
||||
setSaveStatus('saved_local'); // Revert to local save status on error
|
||||
}
|
||||
setSaveStatus('saved');
|
||||
}, 2000); // 2 seconds
|
||||
}
|
||||
}
|
||||
@@ -384,9 +410,10 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
|
||||
|
||||
{/* Save Status Indicator */}
|
||||
<div className="flex items-center gap-2 mr-4 text-xs font-medium text-slate-400">
|
||||
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin" /> Sauvegarde...</>}
|
||||
{saveStatus === 'saved' && <><Check size={12} className="text-green-500" /> Sauvegardé</>}
|
||||
{saveStatus === 'unsaved' && <span className="text-amber-500">Modifications non enregistrées...</span>}
|
||||
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /> <span className="text-blue-500 hidden sm:inline">Sauvegarde en cours...</span></>}
|
||||
{saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /> <span className="text-green-500 hidden sm:inline">Brouillon local</span></>}
|
||||
{saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /> <span className="text-emerald-600 hidden sm:inline">Sauvegardé</span></>}
|
||||
{saveStatus === 'unsaved' && <span className="text-amber-500">Non sauvegardé...</span>}
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-slate-300 mx-1" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { UserProfile, UserPreferences } from '@/lib/types';
|
||||
import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react';
|
||||
import { useLanguage } from '@/providers/LanguageProvider';
|
||||
@@ -30,9 +30,38 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
bio: user.bio || '',
|
||||
email: user.email,
|
||||
theme: user.preferences.theme,
|
||||
dailyWordGoal: user.preferences.dailyWordGoal
|
||||
dailyWordGoal: user.preferences.dailyWordGoal,
|
||||
customColors: user.preferences.customColors || {
|
||||
bg: '#1a1b26',
|
||||
panel: '#24283b',
|
||||
text: '#c0caf5',
|
||||
accent: '#7aa2f7'
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Live Preview of Theme Changes
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-custom');
|
||||
root.classList.add(`theme-${formData.theme}`);
|
||||
|
||||
if (formData.theme === 'custom') {
|
||||
root.style.setProperty('--theme-bg', formData.customColors.bg);
|
||||
root.style.setProperty('--theme-panel', formData.customColors.panel);
|
||||
root.style.setProperty('--theme-text', formData.customColors.text);
|
||||
root.style.setProperty('--theme-border', formData.customColors.text + '20');
|
||||
root.style.setProperty('--theme-muted', formData.customColors.text + '99');
|
||||
root.style.setProperty('--theme-accent', formData.customColors.accent);
|
||||
} else {
|
||||
root.style.removeProperty('--theme-bg');
|
||||
root.style.removeProperty('--theme-panel');
|
||||
root.style.removeProperty('--theme-text');
|
||||
root.style.removeProperty('--theme-border');
|
||||
root.style.removeProperty('--theme-muted');
|
||||
root.style.removeProperty('--theme-accent');
|
||||
}
|
||||
}, [formData.theme, formData.customColors]);
|
||||
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -83,22 +112,29 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
preferences: {
|
||||
...user.preferences,
|
||||
theme: formData.theme,
|
||||
dailyWordGoal: formData.dailyWordGoal
|
||||
dailyWordGoal: formData.dailyWordGoal,
|
||||
customColors: formData.customColors
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('plumeia_theme', formData.theme);
|
||||
if (formData.theme === 'custom') {
|
||||
localStorage.setItem('plumeia_custom_colors', JSON.stringify(formData.customColors));
|
||||
}
|
||||
alert(t('profile.saved_success') || "Profil mis à jour !");
|
||||
};
|
||||
|
||||
const isDark = formData.theme === 'dark';
|
||||
const isSepia = formData.theme === 'sepia';
|
||||
const isCustom = formData.theme === 'custom';
|
||||
|
||||
const themeOuterClass = isDark ? 'bg-slate-900 text-white' : isSepia ? 'bg-[#eaddc4] text-[#433422]' : 'bg-slate-50 text-slate-900';
|
||||
const themeInnerClass = isDark ? 'bg-slate-800 border-slate-700' : isSepia ? 'bg-[#f4ecd8] border-[#dfcdae]' : 'bg-white border-slate-200';
|
||||
const themeTextHeading = isDark ? 'text-white' : isSepia ? 'text-[#332616]' : 'text-slate-900';
|
||||
const themeTextMuted = isDark ? 'text-slate-400' : isSepia ? 'text-[#735e44]' : 'text-slate-500';
|
||||
const themeInputBg = isDark ? 'bg-slate-900 border-slate-700 text-white' : isSepia ? 'bg-[#fbf8f1] border-[#eaddc4] text-[#433422]' : 'bg-slate-50 border-slate-200 text-slate-900';
|
||||
const themeTabActive = isDark ? 'bg-white text-slate-900 shadow-lg' : isSepia ? 'bg-[#5c4731] text-white shadow-lg' : 'bg-slate-900 text-white shadow-lg';
|
||||
const themeTabInactive = isDark ? 'text-slate-400 hover:bg-slate-800 hover:text-white' : isSepia ? 'text-[#735e44] hover:bg-[#eaddc4] hover:text-[#332616]' : 'text-slate-500 hover:bg-white hover:text-slate-900';
|
||||
const themeOuterClass = isCustom ? 'bg-theme-bg text-theme-text' : isDark ? 'bg-slate-900 text-white' : isSepia ? 'bg-[#eaddc4] text-[#433422]' : 'bg-slate-50 text-slate-900';
|
||||
const themeInnerClass = isCustom ? 'bg-theme-panel border-theme-border' : isDark ? 'bg-slate-800 border-slate-700' : isSepia ? 'bg-[#f4ecd8] border-[#dfcdae]' : 'bg-white border-slate-200';
|
||||
const themeTextHeading = isCustom ? 'text-theme-text' : isDark ? 'text-white' : isSepia ? 'text-[#332616]' : 'text-slate-900';
|
||||
const themeTextMuted = isCustom ? 'text-theme-muted' : isDark ? 'text-slate-400' : isSepia ? 'text-[#735e44]' : 'text-slate-500';
|
||||
const themeInputBg = isCustom ? 'bg-theme-bg border-theme-border text-theme-text' : isDark ? 'bg-slate-900 border-slate-700 text-white' : isSepia ? 'bg-[#fbf8f1] border-[#eaddc4] text-[#433422]' : 'bg-slate-50 border-slate-200 text-slate-900';
|
||||
const themeTabActive = isCustom ? 'bg-theme-text text-theme-bg shadow-lg' : isDark ? 'bg-white text-slate-900 shadow-lg' : isSepia ? 'bg-[#5c4731] text-white shadow-lg' : 'bg-slate-900 text-white shadow-lg';
|
||||
const themeTabInactive = isCustom ? 'text-theme-muted hover:bg-theme-panel hover:text-theme-text' : isDark ? 'text-slate-400 hover:bg-slate-800 hover:text-white' : isSepia ? 'text-[#735e44] hover:bg-[#eaddc4] hover:text-[#332616]' : 'text-slate-500 hover:bg-white hover:text-slate-900';
|
||||
|
||||
return (
|
||||
<div className={`h-screen overflow-y-auto p-8 font-sans ${themeOuterClass}`}>
|
||||
@@ -141,7 +177,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
<div className={`flex-1 rounded-2xl shadow-sm border p-8 ${themeInnerClass}`}>
|
||||
{activeTab === 'profile' && (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
<div className={`flex items-center gap-6 pb-8 border-b ${isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
|
||||
<div className={`flex items-center gap-6 pb-8 border-b ${isCustom ? 'border-theme-border' : isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
|
||||
<div className="relative group cursor-pointer" onClick={() => fileInputRef.current?.click()}>
|
||||
<input
|
||||
type="file"
|
||||
@@ -150,7 +186,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
/>
|
||||
<img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" />
|
||||
<img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isCustom ? 'border-theme-panel' : isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" />
|
||||
<div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title={t('profile.change_avatar')}>
|
||||
<Camera size={20} />
|
||||
</div>
|
||||
@@ -212,18 +248,42 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
|
||||
{t('profile.editor_theme')}
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{['light', 'sepia', 'dark'].map((t) => (
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{['light', 'sepia', 'dark', 'custom'].map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setFormData({ ...formData, theme: t as any })}
|
||||
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${formData.theme === t ? 'border-blue-500 bg-blue-50 text-blue-700' : isDark ? 'border-slate-700 hover:border-slate-600' : isSepia ? 'border-[#dfcdae] hover:border-[#cfbd9e]' : 'border-slate-100 hover:border-slate-200'}`}
|
||||
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${formData.theme === t ? isCustom ? 'border-theme-text text-theme-text' : 'border-blue-500 bg-blue-50 text-blue-700' : isCustom ? 'border-theme-border text-theme-muted hover:border-theme-text' : isDark ? 'border-slate-700 hover:border-slate-600' : isSepia ? 'border-[#dfcdae] hover:border-[#cfbd9e]' : 'border-slate-100 hover:border-slate-200'}`}
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-full border border-slate-200 ${t === 'light' ? 'bg-white' : t === 'sepia' ? 'bg-[#f4ecd8]' : 'bg-slate-900'}`} />
|
||||
<div className={`w-8 h-8 rounded-full border border-slate-200 ${t === 'light' ? 'bg-white' : t === 'sepia' ? 'bg-[#f4ecd8]' : t === 'custom' ? 'bg-gradient-to-tr from-pink-500 via-purple-500 to-indigo-500 border-none' : 'bg-slate-900'}`} />
|
||||
<span className={`text-[10px] font-bold uppercase ${formData.theme !== t ? themeTextMuted : ''}`}>{t}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{formData.theme === 'custom' && (
|
||||
<div className={`mt-6 space-y-4 pt-4 border-t ${isCustom ? 'border-theme-border' : 'border-slate-100'}`}>
|
||||
<h4 className={`text-sm font-bold ${themeTextHeading}`}>Couleurs Personnalisées</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
|
||||
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Arrière-plan</label>
|
||||
<input type="color" value={formData.customColors.bg} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, bg: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
|
||||
</div>
|
||||
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
|
||||
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Panneaux</label>
|
||||
<input type="color" value={formData.customColors.panel} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, panel: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
|
||||
</div>
|
||||
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
|
||||
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Texte Principal</label>
|
||||
<input type="color" value={formData.customColors.text} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, text: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
|
||||
</div>
|
||||
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
|
||||
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Détails / Accent</label>
|
||||
<input type="color" value={formData.customColors.accent} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, accent: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,10 +315,10 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mt-12 pt-8 border-t flex justify-end ${isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
|
||||
<div className={`mt-12 pt-8 border-t flex justify-end ${isCustom ? 'border-theme-border' : isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:shadow-blue-200 ${isDark ? 'bg-white text-slate-900 hover:bg-blue-500 hover:text-white' : isSepia ? 'bg-[#5c4731] text-white hover:bg-blue-600' : 'bg-slate-900 text-white hover:bg-blue-600'}`}
|
||||
className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:-translate-y-1 ${isCustom ? 'bg-theme-text text-theme-bg shadow-theme-border' : isDark ? 'bg-white text-slate-900 shadow-slate-900 hover:bg-slate-100' : isSepia ? 'bg-[#5c4731] text-white shadow-[#dfcdae] hover:bg-[#433422]' : 'bg-slate-900 text-white shadow-slate-200 hover:bg-slate-800'}`}
|
||||
>
|
||||
<Save size={18} /> {t('profile.save_changes')}
|
||||
</button>
|
||||
|
||||
@@ -155,7 +155,13 @@ export interface UserUsage {
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
theme: 'light' | 'dark' | 'sepia';
|
||||
theme: 'light' | 'dark' | 'sepia' | 'custom';
|
||||
customColors?: {
|
||||
bg: string;
|
||||
panel: string;
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
dailyWordGoal: number;
|
||||
language: 'fr' | 'en';
|
||||
}
|
||||
|
||||
@@ -7,14 +7,50 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user } = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
// Read from localStorage to apply theme instantly across reloads
|
||||
const savedTheme = localStorage.getItem('plumeia_theme');
|
||||
const savedColorsStr = localStorage.getItem('plumeia_custom_colors');
|
||||
|
||||
const theme = user.preferences?.theme || 'light';
|
||||
const theme = savedTheme || user?.preferences?.theme || 'light';
|
||||
const root = document.documentElement;
|
||||
|
||||
root.classList.remove('theme-light', 'theme-dark', 'theme-sepia');
|
||||
root.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-custom');
|
||||
root.classList.add(`theme-${theme}`);
|
||||
}, [user?.preferences?.theme]);
|
||||
|
||||
if (theme === 'custom') {
|
||||
let colors = user?.preferences?.customColors || {
|
||||
bg: '#ffffff',
|
||||
panel: '#f8fafc',
|
||||
text: '#0f172a',
|
||||
accent: '#3b82f6'
|
||||
};
|
||||
|
||||
if (savedColorsStr) {
|
||||
try {
|
||||
colors = JSON.parse(savedColorsStr);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse custom colors", e);
|
||||
}
|
||||
}
|
||||
|
||||
root.style.setProperty('--theme-bg', colors.bg);
|
||||
root.style.setProperty('--theme-panel', colors.panel);
|
||||
root.style.setProperty('--theme-text', colors.text);
|
||||
|
||||
// To ensure UI remains legible, we compute a translucent border by default if not strictly provided
|
||||
root.style.setProperty('--theme-border', colors.text + '20'); // 20% opacity of text color
|
||||
root.style.setProperty('--theme-muted', colors.text + '99'); // 60% opacity of text color
|
||||
|
||||
root.style.setProperty('--theme-accent', colors.accent);
|
||||
} else {
|
||||
root.style.removeProperty('--theme-bg');
|
||||
root.style.removeProperty('--theme-panel');
|
||||
root.style.removeProperty('--theme-text');
|
||||
root.style.removeProperty('--theme-border');
|
||||
root.style.removeProperty('--theme-muted');
|
||||
root.style.removeProperty('--theme-accent');
|
||||
}
|
||||
}, [user?.preferences?.theme, user?.preferences?.customColors]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user