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:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user