ultisuite-client/components/suite/suite-favorites-menu.tsx
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

225 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useRef, useState } from "react"
import Link from "next/link"
import { Icon } from "@iconify/react"
import { LayoutGrid, Pencil } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
SUITE_FAVORITE_APPS,
type FavoriteApp,
} from "@/lib/suite/favorite-apps"
import { MAIL_SETTINGS_BASE_PATH } from "@/lib/mail-settings/settings-nav"
import { SUITE_APP, suiteAppForRoute, useNativeRuntime } from "@/lib/platform"
import { openSiblingApp } from "@/lib/native/inter-app"
import { SUITE_HEADER_DROPDOWN_CLASS, SUITE_ICON_BTN } from "@/lib/suite/suite-chrome-classes"
import { cn } from "@/lib/utils"
const FAVORITE_TILE_CLASS =
"flex flex-col items-center gap-2 rounded-lg p-3 transition-colors hover:bg-accent"
const DEFAULT_ICON_BTN_CLASS = cn(
"rounded-full",
SUITE_ICON_BTN,
"hover:text-accent-foreground",
)
function FavoriteAppTile({
app,
onNavigate,
}: {
app: FavoriteApp
onNavigate?: () => void
}) {
const content = (
<>
<div className="flex h-10 w-10 items-center justify-center">
{app.iconDark ? (
<>
<img
src={app.icon}
alt=""
className="h-10 w-10 object-contain dark:hidden"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}}
/>
<img
src={app.iconDark}
alt=""
className="hidden h-10 w-10 object-contain dark:block"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}}
/>
</>
) : (
<img
src={app.icon}
alt=""
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 (
<button type="button" className={FAVORITE_TILE_CLASS} disabled>
{content}
</button>
)
}
if (app.external) {
return (
<a
href={app.href}
target="_blank"
rel="noopener noreferrer"
className={FAVORITE_TILE_CLASS}
onClick={onNavigate}
>
{content}
</a>
)
}
// In the native shells, opening another suite product launches the sibling
// app via its deep-link scheme rather than navigating inside this webview.
const targetApp = suiteAppForRoute(app.href)
if (useNativeRuntime() && targetApp && targetApp !== SUITE_APP) {
return (
<button
type="button"
className={FAVORITE_TILE_CLASS}
onClick={() => {
onNavigate?.()
void openSiblingApp(targetApp, app.href!)
}}
>
{content}
</button>
)
}
return (
<Link href={app.href} className={FAVORITE_TILE_CLASS} onClick={onNavigate}>
{content}
</Link>
)
}
interface SuiteFavoritesMenuProps {
className?: string
iconButtonClass?: string
dropdownClass?: string
/** Fermer un autre panneau header (ex. compte) à louverture. */
onOpen?: () => void
}
export function SuiteFavoritesMenu({
className,
iconButtonClass = DEFAULT_ICON_BTN_CLASS,
dropdownClass = SUITE_HEADER_DROPDOWN_CLASS,
onOpen,
}: SuiteFavoritesMenuProps) {
const [open, setOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
return (
<div className={cn("relative hidden sm:block", className)} ref={menuRef}>
<Button
variant="ghost"
size="icon"
className={iconButtonClass}
aria-label="Applications"
aria-expanded={open}
aria-haspopup="dialog"
onClick={() => {
const next = !open
setOpen(next)
if (next) onOpen?.()
}}
>
<LayoutGrid className="size-6 shrink-0" aria-hidden />
</Button>
{open ? (
<div
className={cn(
"absolute right-0 top-12 z-50 w-96 rounded-2xl",
dropdownClass,
)}
role="dialog"
aria-label="Vos favoris"
>
<div className="flex items-center justify-between border-b border-border p-4">
<span className="text-lg font-normal text-foreground">
Vos favoris
</span>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className={cn("h-8 w-8", iconButtonClass)}
aria-label="Réglages"
asChild
>
<Link
href={MAIL_SETTINGS_BASE_PATH}
onClick={() => setOpen(false)}
>
<Icon icon="mdi:cog-outline" className="size-5 shrink-0" aria-hidden />
</Link>
</Button>
<Button
variant="ghost"
size="icon"
className={cn("h-8 w-8", iconButtonClass)}
aria-label="Personnaliser les favoris"
disabled
>
<Pencil className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-3 gap-1 p-3">
{SUITE_FAVORITE_APPS.map((app) => (
<FavoriteAppTile
key={app.name}
app={app}
onNavigate={() => setOpen(false)}
/>
))}
</div>
</div>
) : null}
</div>
)
}