Files
ita-ito-horaires/app/admin/layout.tsx
T
vliaudatandClaude Opus 5 f21268a9f6 feat: add the display messages page with live translation
French in, English out, with the character counter tied to the same
constant the renderer uses — so the warning and the space actually
available on the panel cannot drift apart.

Translation is a second call, not part of the save. A slow or broken
service must never cost the shop its message: the row is stored first
and marked pending, the translation follows, and a failure shows as a
badge with a retry rather than as a lost notice.

The status transitions are the subtle part and are pinned by tests.
Editing the French clears the English, including a translation someone
had corrected by hand — a translation of text that has changed is worse
than no translation. Editing only the dates or the priority leaves it
alone. A hand-written translation is never overwritten while its French
stands, and emptying it returns the row to pending.

"On the screen" is decided by the same selector the renderer uses, so
the badge cannot disagree with the panel about which message is live.

The preview is served by the device's own pipeline — payload, SVG,
threshold — so what the admin sees is the shop window down to the last
thresholded pixel. A preview drawn any other way would eventually
disagree with reality, quietly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 20:16:46 +02:00

81 lines
2.8 KiB
TypeScript

import Image from 'next/image';
import Link from 'next/link';
import { auth, signOut } from '@/lib/auth';
/** Extended as each page lands, so nothing in the bar leads nowhere. */
const NAV = [
{ href: '/admin', label: 'Tableau de bord' },
{ href: '/admin/horaires', label: 'Horaires' },
{ href: '/admin/exceptions', label: 'Exceptions' },
{ href: '/admin/vacances', label: 'Vacances' },
{ href: '/admin/messages', label: 'Messages' },
];
/**
* The admin shell.
*
* The middleware has already established that there is a session by the time
* this renders; the call here is only to know who it is. Navigation links are
* added as their pages land, so nothing in the bar leads nowhere.
*/
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
const email = session?.user?.email ?? '';
const isAdmin = session?.user?.role === 'admin';
return (
<div className="min-h-dvh">
<header className="border-b border-[var(--line)] bg-[var(--surface)]">
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-x-6 gap-y-3 px-4 py-3">
<Link href="/admin" className="flex items-center" aria-label="Tableau de bord">
<Image
src="/brand/logo.png"
alt="ITA ITO"
width={406}
height={194}
className="h-auto w-24"
/>
</Link>
<nav aria-label="Sections" className="flex flex-wrap items-center gap-x-5 gap-y-1">
{NAV.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-sm text-[var(--ink-muted)] underline-offset-4 transition-colors hover:text-[var(--ink)] hover:underline"
>
{item.label}
</Link>
))}
</nav>
<div className="ml-auto flex items-center gap-3 text-sm">
{!isAdmin ? (
<span className="rounded-[var(--radius-sm)] bg-[var(--surface-muted)] px-2 py-1 text-xs text-[var(--ink-muted)]">
Lecture seule
</span>
) : null}
<span className="hidden text-[var(--ink-muted)] sm:inline">{email}</span>
<form
action={async () => {
'use server';
await signOut({ redirectTo: '/login' });
}}
>
<button
type="submit"
className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] px-3 py-1.5 text-sm transition-colors hover:bg-[var(--surface-muted)]"
>
Se déconnecter
</button>
</form>
</div>
</div>
</header>
{children}
</div>
);
}