mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-23 23:28:04 +02:00
Fil d'informations dans l'espace adhérent
L'association pouvait publier des promotions et des rencontres, mais rien à destination de ses seuls adhérents connectés. Un troisième onglet, placé en tête de l'espace, porte désormais ce que publie le bureau : annonces, convocations, rappels. Côté rédaction, `/backend/informations` (capacité `manageInformations`, administrateur et modérateur) : titre, texte balisé avec barre de mise en forme et aperçu, image de couverture, brouillon puis publication, épinglage — une seule information en tête à la fois, appliqué dans l'action plutôt que par un index conditionnel. Côté lecture, `information_reads` porte une ligne par lecture plutôt qu'une date « vu jusqu'ici » sur l'utilisateur : c'est ce qui permet à la fois la pastille « Nouveau » par information et le « lue par 12 / 40 » du back-office. Le marquage passe par une action appelée après affichage : la pastille de l'onglet et celle de la barre latérale sont calculées par deux composants serveur distincts, dont l'ordre de rendu n'est pas garanti, et se seraient contredites sur la même page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QsLjRAnuLwivqxCbM4WeP
This commit is contained in:
19 files changed
+2709
-7
No files matched your search
@@ -13,7 +13,9 @@ export type BackendIconName =
|
||||
| "meetings"
|
||||
| "registrations"
|
||||
| "gallery"
|
||||
| "news"
|
||||
| "emails"
|
||||
| "mailbox"
|
||||
| "social"
|
||||
| "categories"
|
||||
| "settings"
|
||||
@@ -70,6 +72,18 @@ const PATHS: Record<BackendIconName, ReactNode> = {
|
||||
<path d="m4 17 4.8-4.4a1.8 1.8 0 0 1 2.4 0L16 17M14 14.4l1.6-1.5a1.8 1.8 0 0 1 2.4 0L21 15.6" />
|
||||
</>
|
||||
),
|
||||
news: (
|
||||
<>
|
||||
<path d="M17.5 4.5H4.2A1.2 1.2 0 0 0 3 5.7v12a1.8 1.8 0 0 0 1.8 1.8h13.4a2.8 2.8 0 0 0 2.8-2.8V9.2h-3.5V4.5Z" />
|
||||
<path d="M6.2 8.3h8.1M6.2 11.6h8.1M6.2 14.9h5.2" />
|
||||
</>
|
||||
),
|
||||
mailbox: (
|
||||
<>
|
||||
<rect x="2.6" y="5" width="18.8" height="14" rx="2.4" />
|
||||
<path d="m3.4 7.2 7.4 5.3a2 2 0 0 0 2.4 0l7.4-5.3" />
|
||||
</>
|
||||
),
|
||||
emails: (
|
||||
<>
|
||||
<path d="M20.5 3.5 10.8 13.2M20.5 3.5 14.3 21l-3.5-7.8L3 9.7l17.5-6.2Z" />
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { richTextNodes } from "@/lib/rich-text";
|
||||
|
||||
/**
|
||||
* Une information de l'association. Rendu unique, partagé par le fil de
|
||||
* l'espace adhérent et l'aperçu du back-office : une seule carte à maintenir.
|
||||
*
|
||||
* Le corps passe par `richTextNodes()` : des éléments React, jamais du HTML
|
||||
* injecté.
|
||||
*/
|
||||
|
||||
export type InformationCardProps = {
|
||||
title: string;
|
||||
body: string;
|
||||
imageUrl?: string | null;
|
||||
publishedAt?: Date | null;
|
||||
authorName?: string | null;
|
||||
pinned?: boolean;
|
||||
unread?: boolean;
|
||||
/** Mention d'état supplémentaire (« Brouillon », « Lue par 12 / 40 »…). */
|
||||
note?: string | null;
|
||||
};
|
||||
|
||||
function formatDay(value: Date) {
|
||||
return new Date(value).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" });
|
||||
}
|
||||
|
||||
export function InformationCard({
|
||||
title,
|
||||
body,
|
||||
imageUrl,
|
||||
publishedAt,
|
||||
authorName,
|
||||
pinned,
|
||||
unread,
|
||||
note,
|
||||
}: InformationCardProps) {
|
||||
// Une seule accentuation à la fois : l'épinglage prime sur la nouveauté,
|
||||
// sinon les deux liserés se disputeraient le même bord.
|
||||
const accent = pinned ? "#E0A63C" : unread ? "#1F8A5B" : null;
|
||||
|
||||
return (
|
||||
<article
|
||||
style={{
|
||||
background: unread || pinned ? "#fff" : "#fdfcf8",
|
||||
border: `1px solid ${pinned ? "#e5cf9a" : "#e6dcc6"}`,
|
||||
borderRadius: 14,
|
||||
padding: "18px 20px",
|
||||
marginBottom: 14,
|
||||
boxShadow: accent ? `inset 4px 0 0 ${accent}` : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap", marginBottom: 9 }}>
|
||||
{pinned && (
|
||||
<span style={{ background: "#fbeede", color: "#9a6638", borderRadius: 999, padding: "4px 10px", fontSize: 11, fontWeight: 800 }}>
|
||||
Épinglée
|
||||
</span>
|
||||
)}
|
||||
{unread && (
|
||||
<span style={{ background: "#e6f4ec", color: "#1f8a5b", borderRadius: 999, padding: "4px 10px", fontSize: 11, fontWeight: 800 }}>
|
||||
Nouveau
|
||||
</span>
|
||||
)}
|
||||
<span style={{ color: "#9a8d72", fontSize: 12.5 }}>
|
||||
{publishedAt ? `Publié le ${formatDay(publishedAt)}` : "Non publiée"}
|
||||
{authorName ? ` par ${authorName}` : ""}
|
||||
</span>
|
||||
{note && (
|
||||
<span style={{ background: "#f1efe7", color: "#a99c82", borderRadius: 999, padding: "4px 10px", fontSize: 11, fontWeight: 800 }}>
|
||||
{note}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-display" style={{ margin: 0, fontWeight: 700, fontSize: 18, color: "#26201a" }}>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{imageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
style={{ display: "block", width: "100%", maxHeight: 280, objectFit: "cover", borderRadius: 10, border: "1px solid #e6dcc6", margin: "13px 0 4px" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="rich-text" style={{ marginTop: 9 }}>
|
||||
{richTextNodes(body)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { markInformationsRead } from "@/app/backend/actions";
|
||||
|
||||
/**
|
||||
* Marque les informations affichées comme lues, une fois la page rendue.
|
||||
*
|
||||
* Le marquage ne peut pas se faire pendant le rendu de la page : le compteur
|
||||
* de la barre latérale est calculé par le gabarit, un composant serveur
|
||||
* distinct dont l'ordre de rendu n'est pas garanti — la pastille de l'onglet
|
||||
* et celle du menu auraient pu se contredire sur la même page. Ici les deux
|
||||
* retombent ensemble au rafraîchissement.
|
||||
*/
|
||||
export function MarkInformationsRead({ ids }: { ids: number[] }) {
|
||||
const router = useRouter();
|
||||
const handled = useRef("");
|
||||
const key = ids.join(",");
|
||||
|
||||
useEffect(() => {
|
||||
if (!key || handled.current === key) return;
|
||||
handled.current = key;
|
||||
void markInformationsRead(key.split(",").map(Number)).then(() => router.refresh());
|
||||
}, [key, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user