import Link from "next/link"; import { redirect } from "next/navigation"; import type { CSSProperties } from "react"; import { getSession } from "@/lib/session"; import { can } from "@/lib/rbac"; import { countMemberAccounts, getAdminInformations, getInformation } from "@/lib/informations"; import { richTextExcerpt } from "@/lib/rich-text"; import { isMailConfigured } from "@/lib/mail-accounts"; import { mailCountsFor } from "@/lib/mail-outbox"; import { InformationForm } from "./InformationForm"; import { deleteInformation, publishInformation, toggleInformationPin, unpublishInformation, } from "../actions"; export const dynamic = "force-dynamic"; const panel: CSSProperties = { background: "#fff", border: "1px solid #e6dcc6", borderRadius: 14, padding: "17px 18px" }; const eyebrow: CSSProperties = { fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase", color: "#9a8d72", fontWeight: 800, marginBottom: 12 }; const rowButton: CSSProperties = { border: "1px solid #d8cdb4", background: "#fff", color: "#6c6150", fontWeight: 700, fontSize: 12, padding: "6px 11px", borderRadius: 8, cursor: "pointer" }; const pill: CSSProperties = { borderRadius: 999, padding: "4px 10px", fontSize: 11, fontWeight: 800, whiteSpace: "nowrap" }; function formatDay(value: Date) { return new Date(value).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" }); } /** * Rédaction et suivi des informations diffusées dans l'espace adhérent. * * L'information en cours d'édition est désignée par `?modifier=` plutôt * que par un état client : la page reste un composant serveur, et un lien * suffit à passer d'une information à l'autre. */ export default async function InformationsPage({ searchParams, }: { searchParams: Promise<{ modifier?: string; error?: string; ok?: string }>; }) { const session = await getSession(); if (!can(session?.user.role, "manageInformations")) redirect("/backend"); // Un modérateur publie ; écrire à tous les adhérents reste réservé à qui // tient déjà le studio d'e-mails. const canBroadcast = can(session?.user.role, "manageEmails"); const mailReady = canBroadcast && (await isMailConfigured()); const { modifier, error, ok } = await searchParams; const editId = Number(modifier ?? 0) || null; const editing = editId ? await getInformation(editId) : null; const [items, memberCount] = await Promise.all([getAdminInformations(), countMemberAccounts()]); const published = items.filter((item) => item.status === "published"); const drafts = items.filter((item) => item.status === "draft"); const mailCounts = Object.fromEntries( await Promise.all( published .filter((item) => item.emailSentAt) .map(async (item) => [item.id, await mailCountsFor(item.id)] as const) ) ); return (
{error && (
{error}
)} {ok && (
{ok}
)}
Publiées · {published.length}
{published.length === 0 && (
Rien n'est encore visible dans l'espace adhérent.
)}
{published.map((item, index) => (
0 ? { borderTop: "1px solid #f0e8d6", paddingTop: 12 } : undefined}>
{item.title} {item.pinned && Épinglée}
{item.publishedAt ? formatDay(item.publishedAt) : "—"} {" · "} lue par {item.readCount} / {memberCount} {item.authorName ? ` · ${item.authorName}` : ""}
{richTextExcerpt(item.body, 110)}
{item.emailSentAt && (
Diffusée par e-mail · {mailCounts[item.id]?.sent ?? 0} remis {mailCounts[item.id]?.queued ? ` · ${mailCounts[item.id].queued} en file` : ""} {mailCounts[item.id]?.failed ? ` · ${mailCounts[item.id].failed} en échec` : ""}
)}
Modifier
))}
Brouillons · {drafts.length}
{drafts.length === 0 &&
Aucun brouillon en attente.
}
{drafts.map((item, index) => (
0 ? { borderTop: "1px solid #f0e8d6", paddingTop: 12 } : undefined}>
{item.title} Brouillon
Modifié le {formatDay(item.updatedAt)} {item.authorName ? ` · ${item.authorName}` : ""}
{richTextExcerpt(item.body, 110)}
{canBroadcast && ( )}
Modifier
))}
); }