fix: unify theme system - fix theme switching persistence

- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
This commit is contained in:
2026-01-18 22:33:41 +01:00
parent ef60dafd73
commit ddb67ba9e5
306 changed files with 59580 additions and 6063 deletions

View File

@@ -150,14 +150,15 @@ export function Header({
}
useEffect(() => {
const savedTheme = currentUser?.theme || localStorage.getItem('theme') || 'light'
// Use 'theme-preference' to match the unified theme system
const savedTheme = localStorage.getItem('theme-preference') || currentUser?.theme || 'light'
// Don't persist on initial load to avoid unnecessary DB calls
applyTheme(savedTheme, false)
}, [currentUser])
const applyTheme = async (newTheme: string, persist = true) => {
setTheme(newTheme as any)
localStorage.setItem('theme', newTheme)
localStorage.setItem('theme-preference', newTheme)
// Remove all theme classes first
document.documentElement.classList.remove('dark')
@@ -168,7 +169,7 @@ export function Header({
} else if (newTheme !== 'light') {
document.documentElement.setAttribute('data-theme', newTheme)
if (newTheme === 'midnight') {
document.documentElement.classList.add('dark')
document.documentElement.classList.add('dark')
}
}
@@ -244,7 +245,7 @@ export function Header({
const NavItem = ({ href, icon: Icon, label, active, onClick }: any) => {
const content = (
<>
<Icon className={cn("h-5 w-5", active && "fill-current text-amber-900")} />
<Icon className={cn("h-5 w-5", active && "fill-current text-blue-900")} />
{label}
</>
)
@@ -256,7 +257,7 @@ export function Header({
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2 text-left",
active
? "bg-[#EFB162] text-amber-900"
? "bg-blue-100 text-blue-900"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
style={{ minHeight: '44px' }}
@@ -274,7 +275,7 @@ export function Header({
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-[#EFB162] text-amber-900"
? "bg-blue-100 text-blue-900"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
style={{ minHeight: '44px' }}
@@ -289,188 +290,83 @@ export function Header({
return (
<>
<header className="h-20 bg-background/90 backdrop-blur-sm border-b border-transparent flex items-center justify-between px-6 lg:px-12 flex-shrink-0 z-30 sticky top-0">
{/* Mobile Menu Button */}
<Sheet open={isSidebarOpen} onOpenChange={setIsSidebarOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className="lg:hidden mr-4 text-muted-foreground"
aria-label="Open menu"
aria-expanded={isSidebarOpen}
>
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[280px] sm:w-[320px] p-0 pt-4">
<SheetHeader className="px-4 mb-4 flex items-center justify-between">
<SheetTitle className="flex items-center gap-2 text-xl font-normal">
<StickyNote className="h-6 w-6 text-primary" />
{t('nav.workspace')}
</SheetTitle>
<Button
variant="ghost"
size="icon"
onClick={() => setIsSidebarOpen(false)}
className="text-muted-foreground hover:text-foreground"
aria-label="Close menu"
style={{ width: '44px', height: '44px' }}
>
<X className="h-5 w-5" />
</Button>
</SheetHeader>
<div className="flex flex-col gap-1 py-2">
<NavItem
href="/"
icon={StickyNote}
label={t('nav.notes')}
active={pathname === '/' && !hasActiveFilters}
/>
<NavItem
href="/reminders"
icon={Bell}
label={t('reminder.title')}
active={pathname === '/reminders'}
/>
{/* Top Navigation - Style Keep */}
<header className="flex-none flex items-center justify-between whitespace-nowrap border-b border-solid border-slate-200 dark:border-slate-800 bg-white dark:bg-[#1e2128] px-6 py-3 z-20">
<div className="flex items-center gap-8">
<div className="my-2 px-4 flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('labels.title')}</span>
{/* Logo MEMENTO */}
<div className="flex items-center gap-3 text-slate-900 dark:text-white cursor-pointer group" onClick={() => router.push('/')}>
<div className="size-8 bg-blue-500 rounded-lg flex items-center justify-center text-white shadow-sm group-hover:shadow-md transition-all">
<StickyNote className="w-5 h-5" />
</div>
<h2 className="text-xl font-bold leading-tight tracking-tight">MEMENTO</h2>
</div>
{/* Search Bar - Style Keep */}
<label className="hidden md:flex flex-col min-w-40 w-96 !h-10">
<div className="flex w-full flex-1 items-stretch rounded-xl h-full bg-slate-100 dark:bg-slate-800 focus-within:ring-2 focus-within:ring-primary/20 transition-all">
<div className="text-slate-500 dark:text-slate-400 flex items-center justify-center pl-4">
<Search className="w-5 h-5" />
</div>
{labels.map(label => (
<NavItem
key={label.id}
icon={Tag}
label={label.name}
active={currentLabels.includes(label.name)}
onClick={() => toggleLabelFilter(label.name)}
/>
))}
<div className="my-2 border-t border-gray-200 dark:border-zinc-800" />
<NavItem
href="/archive"
icon={Settings}
label={t('nav.archive')}
active={pathname === '/archive'}
/>
<NavItem
href="/trash"
icon={Tag}
label={t('nav.trash')}
active={pathname === '/trash'}
<input
className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl bg-transparent border-none text-slate-900 dark:text-white placeholder:text-slate-500 dark:placeholder:text-slate-400 px-3 text-sm font-medium focus:ring-0"
placeholder={t('search.placeholder') || "Search notes, labels, and more..."}
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</SheetContent>
</Sheet>
{/* Search Bar */}
<div className="flex-1 max-w-2xl flex items-center bg-card rounded-lg px-4 py-3 shadow-sm border border-transparent focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10 transition-all">
<Search className="text-muted-foreground text-xl" />
<input
className="bg-transparent border-none outline-none focus:ring-0 w-full text-sm text-foreground ml-3 placeholder-muted-foreground"
placeholder={t('search.placeholder') || "Search notes, tags, or notebooks..."}
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
{/* IA Search Button */}
<button
onClick={handleSemanticSearch}
disabled={!searchQuery.trim() || isSemanticSearching}
className={cn(
"flex items-center gap-1 px-2 py-1.5 rounded-md text-xs font-medium transition-colors min-h-[36px]",
"hover:bg-accent",
searchParams.get('semantic') === 'true'
? "bg-primary/20 text-primary"
: "text-muted-foreground hover:text-primary",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
title={t('search.semanticTooltip')}
>
<Sparkles className={cn("h-3.5 w-3.5", isSemanticSearching && "animate-spin")} />
</button>
{searchQuery && (
<button
onClick={() => handleSearch('')}
className="ml-2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</label>
</div>
{/* Right Side Actions */}
<div className="flex items-center space-x-3 ml-6">
{/* Label Filter */}
<LabelFilter
selectedLabels={currentLabels}
onFilterChange={handleFilterChange}
/>
<div className="flex flex-1 justify-end gap-4 items-center">
{/* Grid View Button */}
<button className="p-2.5 text-muted-foreground hover:bg-accent rounded-lg transition-colors duration-200 min-h-[44px] min-w-[44px]">
<Grid3x3 className="text-xl" />
{/* Settings Button */}
<button
onClick={() => router.push('/settings')}
className="flex items-center justify-center size-10 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 transition-colors"
>
<Settings className="w-5 h-5" />
</button>
{/* Theme Toggle */}
{/* User Avatar Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="p-2.5 text-muted-foreground hover:bg-accent rounded-lg transition-colors duration-200 min-h-[44px] min-w-[44px]">
{theme === 'light' ? <Sun className="text-xl" /> : <Moon className="text-xl" />}
</button>
<div className="flex items-center justify-center bg-center bg-no-repeat bg-cover rounded-full size-10 ring-2 ring-white dark:ring-slate-700 cursor-pointer shadow-sm hover:shadow-md transition-shadow bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-200"
style={currentUser?.image ? { backgroundImage: `url(${currentUser?.image})` } : undefined}>
{!currentUser?.image && (
<span className="text-sm font-semibold">
{currentUser?.name ? currentUser.name.charAt(0).toUpperCase() : <User className="w-5 h-5" />}
</span>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => applyTheme('light')}>{t('settings.themeLight')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('dark')}>{t('settings.themeDark')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('midnight')}>Midnight</DropdownMenuItem>
<DropdownMenuItem onClick={() => applyTheme('sepia')}>Sepia</DropdownMenuItem>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
{currentUser?.name && <p className="font-medium">{currentUser.name}</p>}
{currentUser?.email && <p className="w-[200px] truncate text-sm text-muted-foreground">{currentUser.email}</p>}
</div>
</div>
<DropdownMenuItem onClick={() => router.push('/settings/profile')} className="cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span>{t('settings.profile') || 'Profile'}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push('/admin')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" />
<span>Admin</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()} className="cursor-pointer text-red-600 focus:text-red-600">
<LogOut className="mr-2 h-4 w-4" />
<span>{t('auth.signOut') || 'Sign out'}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Notifications */}
<NotificationPanel />
{/* User Avatar - Removed from here */}
</div>
</header>
{/* Active Filters Bar */}
{hasActiveFilters && (
<div className="px-6 lg:px-12 pb-3 flex items-center gap-2 overflow-x-auto border-t border-border pt-2 bg-background/50 backdrop-blur-sm animate-in slide-in-from-top-2">
{currentColor && (
<Badge variant="secondary" className="flex items-center gap-1 h-7 whitespace-nowrap pl-2 pr-1">
<div className={cn("w-3 h-3 rounded-full border border-black/10", `bg-${currentColor}-500`)} />
{t('notes.color')}: {currentColor}
<button onClick={removeColorFilter} className="ml-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full p-0.5 min-h-[24px] min-w-[24px]">
<X className="h-3 w-3" />
</button>
</Badge>
)}
{currentLabels.map(label => (
<Badge key={label} variant="secondary" className="flex items-center gap-1 h-7 whitespace-nowrap pl-2 pr-1">
<Tag className="h-3 w-3" />
{label}
<button onClick={() => removeLabelFilter(label)} className="ml-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full p-0.5">
<X className="h-3 w-3" />
</button>
</Badge>
))}
{(currentLabels.length > 0 || currentColor) && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="h-7 text-xs text-primary hover:text-primary hover:bg-accent whitespace-nowrap ml-auto"
>
{t('labels.clearAll')}
</Button>
)}
</div>
)}
</>
)
}