ultisuite-client/lib/suite/page-metadata.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

209 lines
6.4 KiB
TypeScript

import type { Metadata } from "next"
import { displayFileName } from "@/lib/drive/display-file-name"
import { parseDriveSegments } from "@/lib/drive/drive-url"
export type SuiteApp = "mail" | "drive" | "contacts" | "agenda" | "meet" | "admin" | "compte" | "suite"
/** Separator between page segment and product name in document titles. */
export const SUITE_TITLE_SEP = " - "
export const MAIL_INBOX_DOCUMENT_TITLE = `Boîte mail${SUITE_TITLE_SEP}Ultimail`
const DESCRIPTIONS: Record<SuiteApp, string> = {
mail: "Client mail Ultimail — suite souveraine",
drive: "Stockage de fichiers UltiDrive — suite UltiSuite",
contacts: "Carnet d'adresses — UltiSuite",
agenda: "Agenda partagé, invitations et disponibilités — UltiSuite",
meet: "Visioconférence chiffrée intégrée à la suite — UltiMeet",
admin: "Console d'administration — UltiSuite",
compte: "Réglages du compte — UltiSuite",
suite: "Ultimail, UltiDrive et contacts — interface suite unifiée",
}
const APP_LABELS: Record<SuiteApp, string> = {
mail: "Ultimail",
drive: "UltiDrive",
contacts: "UltiSuite",
agenda: "Agenda",
meet: "UltiMeet",
admin: "Administration",
compte: "Compte Ulti",
suite: "UltiSuite",
}
const ICONS: Record<SuiteApp, Metadata["icons"]> = {
suite: {
icon: [
{ url: "/ultisuite-mark.svg", type: "image/svg+xml" },
{ url: "/ultisuite-32.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/ultisuite-180.png", sizes: "180x180", type: "image/png" }],
shortcut: "/ultisuite-mark.svg",
},
admin: {
icon: [{ url: "/admin-mark.svg", type: "image/svg+xml" }],
apple: [{ url: "/admin-mark.svg", type: "image/svg+xml" }],
shortcut: "/admin-mark.svg",
},
mail: {
icon: [
{ url: "/ultimail-mark.svg", type: "image/svg+xml" },
{ url: "/icon.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/apple-icon.png", sizes: "180x180", type: "image/png" }],
shortcut: "/ultimail-mark.svg",
},
drive: {
icon: [
{ url: "/ultidrive-mark.svg", type: "image/svg+xml" },
{ url: "/drive/favicon-32.png", sizes: "32x32", type: "image/png" },
{ url: "/drive/favicon-16.png", sizes: "16x16", type: "image/png" },
],
apple: [{ url: "/drive/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
shortcut: "/ultidrive-mark.svg",
},
contacts: {
icon: [
{ url: "/contacts-mark.svg", type: "image/svg+xml" },
{ url: "/icon.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/contacts-mark.svg", type: "image/svg+xml" }],
shortcut: "/contacts-mark.svg",
},
compte: {
icon: [{ url: "/compte-mark.svg", type: "image/svg+xml" }],
apple: [{ url: "/compte-mark.svg", type: "image/svg+xml" }],
shortcut: "/compte-mark.svg",
},
agenda: {
icon: [
{ url: "/agenda-mark.svg", type: "image/svg+xml" },
{ url: "/icon.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/agenda-mark.svg", type: "image/svg+xml" }],
shortcut: "/agenda-mark.svg",
},
meet: {
icon: [
{ url: "/ultimeet-mark.svg", type: "image/svg+xml" },
{ url: "/icon.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/ultimeet-mark.svg", type: "image/svg+xml" }],
shortcut: "/ultimeet-mark.svg",
},
}
type PageMetadataOptions = {
app: SuiteApp
/** Full document title (no suffix added). */
title?: string
/** Short segment only — parent layout `title.template` adds the product suffix. */
titleSegment?: string
description?: string
/** When true with `title`, use `title` as the full document title. */
absoluteTitle?: boolean
}
export function formatSuiteDocumentTitle(segment: string, app: SuiteApp): string {
return `${segment}${SUITE_TITLE_SEP}${APP_LABELS[app]}`
}
export function truncateSubjectForTitle(subject: string, maxLen = 48): string {
const clean = subject.replace(/\s+/g, " ").trim()
if (!clean) return ""
if (clean.length <= maxLen) return clean
return `${clean.slice(0, maxLen - 1).trimEnd()}`
}
export function mailDocumentTitle(subject: string | null | undefined): string {
const trimmed = subject?.replace(/\s+/g, " ").trim()
if (trimmed) {
return formatSuiteDocumentTitle(truncateSubjectForTitle(trimmed), "mail")
}
return MAIL_INBOX_DOCUMENT_TITLE
}
export function suiteRootMetadata(): Metadata {
return {
title: APP_LABELS.suite,
description: DESCRIPTIONS.suite,
icons: ICONS.suite,
}
}
export function suitePageMetadata({
app,
title,
titleSegment,
description,
absoluteTitle = false,
}: PageMetadataOptions): Metadata {
const label = APP_LABELS[app]
let resolvedTitle: Metadata["title"]
if (titleSegment !== undefined) {
// Short segment — nearest ancestor `title.template` adds the product suffix once.
resolvedTitle = titleSegment
} else if (title === undefined) {
resolvedTitle = {
default: label,
template: `%s${SUITE_TITLE_SEP}${label}`,
}
} else if (absoluteTitle) {
resolvedTitle = { absolute: title }
} else {
// Full title already includes suffix — absolute so root/ancestor templates do not re-apply.
resolvedTitle = { absolute: formatSuiteDocumentTitle(title, app) }
}
const icons = ICONS[app]
const meta: Metadata = {
title: resolvedTitle,
description: description ?? DESCRIPTIONS[app],
icons,
}
if (app === "drive") {
meta.manifest = "/drive/manifest.webmanifest"
meta.appleWebApp = {
capable: true,
title: "UltiDrive",
statusBarStyle: "default",
}
}
return meta
}
export function driveDocumentTitle(segments?: string[]): string {
const route = parseDriveSegments(segments)
switch (route.view) {
case "recent":
return "Récents"
case "starred":
return "Favoris"
case "trash":
return "Corbeille"
case "shared":
return route.pathSegments.length > 0
? displayFileName(route.pathSegments.at(-1)!)
: "Partagés avec moi"
case "search":
return "Recherche"
case "org":
return route.pathSegments.length > 0
? displayFileName(route.pathSegments.at(-1)!)
: "Dossier d'organisation"
case "mount":
return route.pathSegments.length > 0
? displayFileName(route.pathSegments.at(-1)!)
: "Volume monté"
default:
if (route.pathSegments.length > 0) {
return displayFileName(route.pathSegments.at(-1)!)
}
return "Mon Drive"
}
}