ultisuite-client/components/gmail/header-account-actions.tsx
R3D347HR4Y c87670e90f
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
feat(api): offline-first mail sync w/ TanStack Query
Move mail, compose, contacts, and accounts off mocks onto REST + WS.
Add client, auth store, IDB-backed query cache, offline queue, and
sync bar; hybrid Zustand for UI-only state. Settings still local until
backend has preferences API.
2026-05-23 00:04:28 +02:00

205 lines
6.4 KiB
TypeScript

"use client"
import { useState, useRef, useEffect } from "react"
import Link from "next/link"
import { Icon, addCollection } from "@iconify/react"
import { icons as mdiIcons } from "@iconify-json/mdi"
import { Pencil } from "lucide-react"
import { AccountAvatar } from "@/components/gmail/account-avatar"
import { AccountSwitcherDropdown } from "@/components/gmail/account-switcher-dropdown"
import { Button } from "@/components/ui/button"
import { useActiveAccount } from "@/lib/stores/account-store"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
import { MAIL_HEADER_DROPDOWN_CLASS, MAIL_ICON_BTN } from "@/lib/mail-chrome-classes"
import { cn } from "@/lib/utils"
const HEADER_ICON_BTN_CLASS = cn(
"rounded-full",
MAIL_ICON_BTN,
"hover:text-accent-foreground",
)
addCollection(mdiIcons)
type FavoriteApp = {
name: string
icon: string
href?: string
/** Logos sombres : blanc en dark via invert + hue-rotate. */
whiteLogoInDark?: boolean
}
const googleApps: FavoriteApp[] = [
{ name: "Compte", icon: "/compte-mark.svg" },
{ name: "Agenda", icon: "/agenda-mark.svg" },
{ name: "Photos", icon: "/photos-mark.svg" },
{ name: "Ultimail", icon: "/brand/ultimail-header-icon.png", href: "/mail" },
{ name: "UltiDrive", icon: "/ultidrive-mark.svg" },
{ name: "UltiMeet", icon: "/ultimeet-mark.svg" },
{ name: "Administration", icon: "/admin-mark.svg" },
{ name: "OpenMaps", icon: "/openstreetmap-mark.svg" },
{ name: "Mistral", icon: "/mistral-mark.svg" },
{ name: "Qwant", icon: "/qwant-mark.svg", whiteLogoInDark: true },
{ name: "Ground News", icon: "/ground-news-mark.svg", whiteLogoInDark: true },
]
const FAVORITE_TILE_CLASS =
"flex flex-col items-center gap-2 rounded-lg p-3 transition-colors hover:bg-accent"
function FavoriteAppTile({ app }: { app: FavoriteApp }) {
const content = (
<>
<div className="flex h-10 w-10 items-center justify-center">
<img
src={app.icon}
alt={app.name}
className={cn(
"h-10 w-10 object-contain",
app.whiteLogoInDark && "dark:invert dark:hue-rotate-180",
)}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
target.parentElement!.innerHTML = `<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-500 font-bold text-white">${app.name[0]}</div>`
}}
/>
</div>
<span className="w-full text-center text-xs text-muted-foreground">{app.name}</span>
</>
)
if (app.href) {
return (
<Link href={app.href} className={FAVORITE_TILE_CLASS}>
{content}
</Link>
)
}
return (
<button type="button" className={FAVORITE_TILE_CLASS}>
{content}
</button>
)
}
interface HeaderAccountActionsProps {
className?: string
}
export function HeaderAccountActions({ className }: HeaderAccountActionsProps) {
const [appsMenuOpen, setAppsMenuOpen] = useState(false)
const [accountMenuOpen, setAccountMenuOpen] = useState(false)
const appsMenuRef = useRef<HTMLDivElement>(null)
const accountMenuRef = useRef<HTMLDivElement>(null)
const activeAccount = useActiveAccount()
const openQuickSettings = useMailSettingsStore((s) => s.setQuickSettingsOpen)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
appsMenuRef.current &&
!appsMenuRef.current.contains(event.target as Node)
) {
setAppsMenuOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
return (
<div className={cn("flex shrink-0 items-center gap-1", className)}>
<Button
variant="ghost"
size="icon"
className={cn("hidden sm:inline-flex", HEADER_ICON_BTN_CLASS)}
aria-label="Aide"
>
<Icon
icon="mdi:help-circle-outline"
className="size-6 shrink-0"
aria-hidden
/>
</Button>
<Button
variant="ghost"
size="icon"
className={HEADER_ICON_BTN_CLASS}
aria-label="Réglages"
onClick={() => openQuickSettings(true)}
>
<Icon icon="mdi:cog-outline" className="size-6 shrink-0" aria-hidden />
</Button>
<div className="relative hidden sm:block" ref={appsMenuRef}>
<Button
variant="ghost"
size="icon"
className={HEADER_ICON_BTN_CLASS}
aria-label="Applications"
onClick={() => {
setAppsMenuOpen(!appsMenuOpen)
setAccountMenuOpen(false)
}}
>
<Icon
icon="mdi:view-grid-outline"
className="size-6 shrink-0"
aria-hidden
/>
</Button>
{appsMenuOpen && (
<div
className={cn(
"absolute right-0 top-12 z-50 w-96 rounded-2xl",
MAIL_HEADER_DROPDOWN_CLASS,
)}
>
<div className="flex items-center justify-between border-b border-border p-4">
<span className="text-lg font-normal text-foreground">
Vos favoris
</span>
<Button
variant="ghost"
size="icon"
className={cn("h-8 w-8", HEADER_ICON_BTN_CLASS)}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-3 gap-1 p-3">
{googleApps.map((app) => (
<FavoriteAppTile key={app.name} app={app} />
))}
</div>
</div>
)}
</div>
<div className="relative ml-2" ref={accountMenuRef}>
<Button
variant="ghost"
size="icon-lg"
className="size-11 overflow-hidden rounded-full p-0"
aria-label={`Compte : ${activeAccount?.email ?? ""}`}
aria-expanded={accountMenuOpen}
aria-haspopup="dialog"
onClick={() => {
setAccountMenuOpen(!accountMenuOpen)
setAppsMenuOpen(false)
}}
>
{activeAccount && <AccountAvatar account={activeAccount} size="md" />}
</Button>
<AccountSwitcherDropdown
open={accountMenuOpen}
onOpenChange={setAccountMenuOpen}
containerRef={accountMenuRef}
/>
</div>
</div>
)
}