mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
263 lines
11 KiB
React
263 lines
11 KiB
React
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '@/contexts/AuthContext'
|
|
import { useTheme } from '@/contexts/ThemeContext'
|
|
import { useProfile } from '@/contexts/ProfileProvider'
|
|
import { useDateTimePreference } from '@/contexts/DateTimePreferenceContext'
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
|
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
|
import { AlertDialog, AlertDialogContent, } from '@/components/ui/alert-dialog'
|
|
|
|
import { User, Settings, LogOut, Sun, Moon, Monitor, Loader2, Check, Clock } from 'lucide-react'
|
|
|
|
import { AVATAR_COLORS } from '@/data/profile.data'
|
|
|
|
// ─── Theme Option ─────────────────────────────────────────────────────────────
|
|
function ThemeOption({ value, label, icon: Icon, active, onClick }) {
|
|
return (
|
|
<button
|
|
onClick={() => onClick(value)}
|
|
className={`relative flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all cursor-pointer w-full
|
|
${active
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border hover:border-muted-foreground/40 hover:bg-muted/40'
|
|
}`}
|
|
>
|
|
<Icon size={18} className={active ? 'text-primary' : 'text-muted-foreground'} />
|
|
<span className={`text-xs font-medium ${active ? 'text-primary' : 'text-muted-foreground'}`}>
|
|
{label}
|
|
</span>
|
|
{active && (
|
|
<span className="absolute top-2 right-2">
|
|
<Check size={12} className="text-primary" />
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
// ─── Timezone Option ──────────────────────────────────────────────────────────
|
|
function TimezoneOption({ value, label, description, active, onClick }) {
|
|
return (
|
|
<button
|
|
onClick={() => onClick(value)}
|
|
className={`flex flex-col gap-1 p-3 rounded-lg border-2 transition-all cursor-pointer w-full text-left
|
|
${active
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border hover:border-muted-foreground/40 hover:bg-muted/40'
|
|
}`}
|
|
>
|
|
<span className={`text-sm font-semibold ${active ? 'text-primary' : 'text-foreground'}`}>
|
|
{label}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">{description}</span>
|
|
{active && <Check size={12} className="text-primary mt-0.5" />}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
// ─── Settings Dialog ──────────────────────────────────────────────────────────
|
|
function SettingsDialog({ open, onClose }) {
|
|
const { theme, setTheme } = useTheme()
|
|
const { timezone, setTimezone } = useDateTimePreference()
|
|
const [tab, setTab] = useState('appearance')
|
|
|
|
const TABS = [
|
|
{ id: 'appearance', label: 'Appearance', icon: Sun },
|
|
{ id: 'datetime', label: 'Date & Time', icon: Clock },
|
|
]
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="p-0 overflow-hidden sm:max-w-2xl gap-0 rounded-2xl">
|
|
<div className="flex h-[480px]">
|
|
|
|
{/* ── Sidebar ── */}
|
|
<div className="w-52 shrink-0 border-r bg-muted/30 flex flex-col">
|
|
<div className="flex items-center px-4 py-3 border-b">
|
|
<span className="text-sm font-semibold">Settings</span>
|
|
</div>
|
|
<nav className="flex-1 p-2 space-y-0.5">
|
|
{TABS.map((t) => (
|
|
<button
|
|
key={t.id}
|
|
onClick={() => setTab(t.id)}
|
|
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm font-medium transition-colors text-left
|
|
${tab === t.id
|
|
? 'bg-background text-foreground shadow-sm border border-border/50'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/60'
|
|
}`}
|
|
>
|
|
<t.icon size={14} />
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* ── Content ── */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
|
|
{/* Appearance */}
|
|
{tab === 'appearance' && (
|
|
<div className="space-y-5">
|
|
<div>
|
|
<h2 className="text-base font-semibold">Appearance</h2>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
Choose how the interface looks for you.
|
|
</p>
|
|
</div>
|
|
<Separator />
|
|
<div>
|
|
<Label className="text-sm font-medium mb-3 block">Theme</Label>
|
|
<div className="grid grid-cols-3 gap-3 relative">
|
|
<ThemeOption value="light" label="Light" icon={Sun} active={theme === 'light'} onClick={setTheme} />
|
|
<ThemeOption value="dark" label="Dark" icon={Moon} active={theme === 'dark'} onClick={setTheme} />
|
|
<ThemeOption value="system" label="System" icon={Monitor} active={theme === 'system'} onClick={setTheme} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Date & Time */}
|
|
{tab === 'datetime' && (
|
|
<div className="space-y-5">
|
|
<div>
|
|
<h2 className="text-base font-semibold">Date & Time</h2>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
Choose how dates and times are displayed across the platform.
|
|
Your locale format is taken from your browser's language setting.
|
|
</p>
|
|
</div>
|
|
<Separator />
|
|
<div>
|
|
<Label className="text-sm font-medium mb-3 block">Timezone Display</Label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<TimezoneOption
|
|
value="local"
|
|
label="Local"
|
|
description="Dates shown in your device's local timezone."
|
|
active={timezone === 'local'}
|
|
onClick={setTimezone}
|
|
/>
|
|
<TimezoneOption
|
|
value="UTC"
|
|
label="UTC"
|
|
description="Dates shown in Coordinated Universal Time (UTC+0)."
|
|
active={timezone === 'UTC'}
|
|
onClick={setTimezone}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="rounded-lg border bg-muted/40 p-3 text-xs text-muted-foreground space-y-1">
|
|
<p className="font-medium text-foreground">Preview</p>
|
|
<p>Date: <span className="text-foreground">{new Date().toLocaleDateString(navigator.language, { month: 'short', day: 'numeric', year: 'numeric', ...(timezone === 'UTC' ? { timeZone: 'UTC' } : {}) })}</span></p>
|
|
<p>Date & Time: <span className="text-foreground">{new Date().toLocaleString(navigator.language, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', ...(timezone === 'UTC' ? { timeZone: 'UTC' } : {}) })}</span></p>
|
|
{timezone === 'UTC' && <p className="text-amber-600 dark:text-amber-400 font-medium">All times shown in UTC</p>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// ─── Sign Out Overlay ─────────────────────────────────────────────────────────
|
|
function SignOutOverlay({ open }) {
|
|
return (
|
|
<AlertDialog open={open}>
|
|
<AlertDialogContent className="max-w-xs">
|
|
<div className="flex flex-col items-center justify-center gap-4 py-6">
|
|
<Loader2 className="size-8 animate-spin text-muted-foreground" />
|
|
<p className="text-sm font-medium">Signing out...</p>
|
|
</div>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
)
|
|
}
|
|
|
|
// ─── Main UserMenu ────────────────────────────────────────────────────────────
|
|
export default function UserMenu() {
|
|
const { user, logout } = useAuth()
|
|
const { avatarUrl } = useProfile()
|
|
const navigate = useNavigate()
|
|
|
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
const [signingOut, setSigningOut] = useState(false)
|
|
|
|
// Build initials from personal_info or fallback to acc_type first char
|
|
const given = user?.personal_info?.name?.given_name ?? ''
|
|
const last = user?.personal_info?.name?.last_name ?? ''
|
|
|
|
const initials = given && last
|
|
? (given[0] + last[0]).toUpperCase()
|
|
: (user?.email?.[0] ?? 'U').toUpperCase()
|
|
|
|
const fullName = given && last ? `${given} ${last}` : user?.email ?? 'User'
|
|
const shortName = (given || user?.email) ?? 'User'
|
|
const avatarColor = AVATAR_COLORS[user?.acc_type] ?? 'bg-muted text-muted-foreground'
|
|
|
|
const handleLogout = async () => {
|
|
setSigningOut(true)
|
|
await logout()
|
|
navigate('/login', { replace: true })
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Avatar className="rounded-lg cursor-pointer hover:opacity-80 transition-opacity size-9">
|
|
<AvatarImage src={avatarUrl ?? ''} alt={fullName} />
|
|
<AvatarFallback className={`text-sm font-semibold ${avatarColor}`}>
|
|
{initials}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="end" className="w-64">
|
|
{/* User info */}
|
|
<div className="px-2 py-2">
|
|
<p className="text-sm font-semibold truncate">{shortName}</p>
|
|
<p className="text-xs text-muted-foreground truncate">{user?.email ?? '—'}</p>
|
|
</div>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem onClick={() => navigate('my-profile')}>
|
|
<User className="size-4" />
|
|
<span className="font-medium">Profile</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setSettingsOpen(true)}>
|
|
<Settings className="size-4" />
|
|
<span className="font-medium">Settings</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem variant="destructive" onClick={handleLogout}>
|
|
<LogOut className="size-4" />
|
|
<span className="font-medium">Sign out</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Settings dialog */}
|
|
<SettingsDialog open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
|
|
|
{/* Sign out overlay */}
|
|
<SignOutOverlay open={signingOut} />
|
|
</>
|
|
)
|
|
} |