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
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:
+77
-11
@@ -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'hui
|
||||
</p>
|
||||
{notes.length === 0 ? (
|
||||
<p className="text-center text-sm text-marble/60">Le mur est encore vide aujourd'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'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}
|
||||
|
||||
Reference in New Issue
Block a user