Permet de consulter les jours précédents du Mur de la Honte, pour tout le monde
Build and deploy / deploy (push) Successful in 36s

wall_notes_today() devient wall_notes_for_date(p_date default null) — même garantie d'anonymat qu'avant, juste paramétrée par jour plutôt que figée sur aujourd'hui. Nouvelle RPC wall_note_days() (dates ayant au moins une note, sans contenu ni auteur) peuple un sélecteur de jour au-dessus de la grille, visible par tous — pas seulement les Archontes via l'historique complet, qui lui révèle l'auteur. wall_note_reaction_counts() gagne le même paramètre p_date. Le compteur "X/12 aujourd'hui" et l'éligibilité à publier restent toujours basés sur aujourd'hui, peu importe le jour consulté.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-25 18:15:34 +02:00
parent 203afafec2
commit 0eaebb7a05
4 changed files with 134 additions and 32 deletions
+77 -11
View File
@@ -149,6 +149,7 @@ export function MurView({
initialHistory,
initialReactionCounts,
initialMyReactions,
availableDays,
}: {
isJudge: boolean;
currentUserId: string;
@@ -157,6 +158,7 @@ export function MurView({
initialHistory: WallHistoryRow[];
initialReactionCounts: ReactionCountRow[];
initialMyReactions: MyReactionRow[];
availableDays: string[];
}) {
const [notes, setNotes] = useState(initialNotes);
const [myNoteToday, setMyNoteToday] = useState(initialMyNoteToday);
@@ -171,6 +173,15 @@ export function MurView({
const [error, setError] = useState<string | null>(null);
const [historyDateFilter, setHistoryDateFilter] = useState("all");
const [zoomedImage, setZoomedImage] = useState<string | null>(null);
// Jour affiché dans la grille du haut (ouverte à tout le monde,
// contrairement à l'historique complet réservé aux Archontes plus bas) :
// null = aujourd'hui, réactif au Realtime comme avant. Un jour passé se
// charge à la demande (pas de Realtime dessus, un jour passé n'évolue
// plus) dans un état séparé pour ne jamais mélanger les deux.
const [viewedDate, setViewedDate] = useState<string | null>(null);
const [pastNotes, setPastNotes] = useState<WallNoteRow[]>([]);
const [pastReactionCounts, setPastReactionCounts] = useState<Record<string, Record<string, number>>>({});
const [pastLoading, setPastLoading] = useState(false);
const refetch = useCallback(async () => {
const supabase = createClient();
@@ -228,6 +239,27 @@ export function MurView({
};
}, [refetch]);
const fetchDay = useCallback(async (dateKey: string) => {
const supabase = createClient();
const [{ data: dayNotes }, { data: dayCounts }] = await Promise.all([
supabase.rpc("wall_notes_for_date", { p_date: dateKey }),
supabase.rpc("wall_note_reaction_counts", { p_date: dateKey }),
]);
setPastNotes(dayNotes ?? []);
setPastReactionCounts(buildReactionCounts(dayCounts ?? []));
}, []);
async function handleSelectDate(value: string) {
if (value === "today") {
setViewedDate(null);
return;
}
setViewedDate(value);
setPastLoading(true);
await fetchDay(value);
setPastLoading(false);
}
function handleImageChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
event.target.value = "";
@@ -318,7 +350,11 @@ export function MurView({
}
async function handleToggleReaction(noteId: string, emoji: string) {
// Optimiste : le canal Realtime confirmera (ou corrigera) juste après.
// Optimiste : le canal Realtime confirmera (ou corrigera) juste après
// sauf en dehors d'aujourd'hui, où il n'y a pas de Realtime (un jour
// passé n'évolue plus) : on corrige alors en rechargeant ce jour-là.
const setCounts = viewedDate === null ? setReactionCounts : setPastReactionCounts;
setMyReactions((current) => {
const next = new Set(current[noteId] ?? []);
const wasActive = next.has(emoji);
@@ -326,7 +362,7 @@ export function MurView({
else next.add(emoji);
return { ...current, [noteId]: next };
});
setReactionCounts((current) => {
setCounts((current) => {
const wasActive = myReactions[noteId]?.has(emoji) ?? false;
const noteCounts = { ...(current[noteId] ?? {}) };
noteCounts[emoji] = Math.max(0, (noteCounts[emoji] ?? 0) + (wasActive ? -1 : 1));
@@ -338,11 +374,22 @@ export function MurView({
p_note_id: noteId,
p_emoji: emoji,
});
if (rpcError) refetch();
if (rpcError) {
if (viewedDate === null) refetch();
else fetchDay(viewedDate);
}
}
// wallFull/le compteur restent toujours basés sur aujourd'hui (notes),
// jamais sur le jour affiché (displayedNotes) : consulter un jour passé
// ne doit jamais changer l'éligibilité à publier aujourd'hui.
const wallFull = notes.length >= MAX_NOTES_PER_DAY;
const todayKey = new Date().toLocaleDateString("sv-SE", { timeZone: "Europe/Paris" });
const pastDays = availableDays.filter((day) => day !== todayKey);
const displayedNotes = viewedDate === null ? notes : pastNotes;
const displayedReactionCounts = viewedDate === null ? reactionCounts : pastReactionCounts;
const historyDates = Array.from(new Set(history.map((n) => localDateKey(n.created_at)))).sort((a, b) =>
b.localeCompare(a),
);
@@ -352,19 +399,38 @@ export function MurView({
return (
<div className="flex flex-col gap-6">
<div>
<p className="mb-3 text-center text-xs font-medium tracking-wide text-marble/60 uppercase">
{notes.length} / {MAX_NOTES_PER_DAY}{" "}
aujourd&apos;hui
</p>
{notes.length === 0 ? (
<p className="text-center text-sm text-marble/60">Le mur est encore vide aujourd&apos;hui.</p>
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<p className="text-xs font-medium tracking-wide text-marble/60 uppercase">
{viewedDate === null ? `${notes.length} / ${MAX_NOTES_PER_DAY} aujourd'hui` : formatDateLabel(viewedDate)}
</p>
{pastDays.length > 0 && (
<select
value={viewedDate ?? "today"}
onChange={(event) => handleSelectDate(event.target.value)}
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1 text-xs text-text-marble outline-none focus:border-gold"
>
<option value="today">Aujourd&apos;hui</option>
{pastDays.map((day) => (
<option key={day} value={day}>
{formatDateLabel(day)}
</option>
))}
</select>
)}
</div>
{pastLoading ? (
<p className="text-center text-sm text-marble/60">Chargement</p>
) : displayedNotes.length === 0 ? (
<p className="text-center text-sm text-marble/60">
{viewedDate === null ? "Le mur est encore vide aujourd'hui." : "Le mur était vide ce jour-là."}
</p>
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{notes.map((note) => (
{displayedNotes.map((note) => (
<NoteCard
key={note.id}
note={note}
reactionCounts={reactionCounts[note.id] ?? {}}
reactionCounts={displayedReactionCounts[note.id] ?? {}}
myReactions={myReactions[note.id] ?? new Set()}
onToggleReaction={(emoji) => handleToggleReaction(note.id, emoji)}
onImageClick={setZoomedImage}