Files
tribunal-app/src/app/calendrier/calendrier-view.tsx
T
Valentin ROBIN 1eef52a101
Build and deploy / deploy (push) Successful in 35s
Retire Le Calendrier des Dieux du menu, affiche la date du Tribunal sur L'Agora
/calendrier disparaît de la navigation (route toujours fonctionnelle
pour les Archontes qui y accèdent directement) : la date du Tribunal,
seule info largement utile au quotidien, est reprise en en-tête de
/leaderboard. Extrait la logique de décompte dans un helper partagé
src/lib/tribunal-date.ts plutôt que de la dupliquer.

Corrige au passage un espace JSX manquant sur /urne oublié lors du
dernier commit ("Tu as voté pour Xaujourd'hui").
2026-08-23 19:29:31 +02:00

288 lines
10 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
import { DayCard, type DayRow } from "./day-card";
import { PANTHEON_PRESETS, getGodPreset } from "@/lib/pantheon";
import { IconPlus } from "@/components/icons";
import { formatCountdown } from "@/lib/tribunal-date";
export function CalendrierView({
isJudge,
initialDays,
initialTribunalDate,
}: {
isJudge: boolean;
initialDays: DayRow[];
initialTribunalDate: string | null;
}) {
const [days, setDays] = useState<DayRow[]>(initialDays);
const [tribunalDate, setTribunalDate] = useState<string | null>(initialTribunalDate);
const [editingTribunalDate, setEditingTribunalDate] = useState(false);
const [tribunalInput, setTribunalInput] = useState(
initialTribunalDate ? initialTribunalDate.slice(0, 16) : "",
);
const [addingDay, setAddingDay] = useState(false);
const [newDate, setNewDate] = useState("");
const [newGod, setNewGod] = useState(PANTHEON_PRESETS[0].name);
const [newGodDomain, setNewGodDomain] = useState(PANTHEON_PRESETS[0].domain);
const [customGod, setCustomGod] = useState(false);
const [error, setError] = useState<string | null>(null);
async function refetch() {
const supabase = createClient();
const { data } = await supabase
.from("days")
.select("id, date, god_name, god_domain, description, events(*)")
.order("date", { ascending: true });
if (data) {
setDays(
data.map((day) => ({
...day,
events: [...day.events].sort((a, b) =>
(a.start_time ?? "99:99").localeCompare(b.start_time ?? "99:99"),
),
})),
);
}
const { data: settingsRow } = await supabase
.from("settings")
.select("tribunal_date")
.eq("id", true)
.single();
setTribunalDate(settingsRow?.tribunal_date ?? null);
}
useEffect(() => {
const supabase = createClient();
let channel: ReturnType<typeof supabase.channel> | null = null;
let cancelled = false;
waitForRealtimeAuth(supabase).then(() => {
if (cancelled) return;
channel = supabase
.channel("calendrier-changes")
.on("postgres_changes", { event: "*", schema: "public", table: "days" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "events" }, refetch)
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
.subscribe();
});
return () => {
cancelled = true;
if (channel) supabase.removeChannel(channel);
};
}, []);
async function handleSaveTribunalDate() {
const supabase = createClient();
const isoValue = tribunalInput ? new Date(tribunalInput).toISOString() : null;
const { error: updateError } = await supabase
.from("settings")
.update({ tribunal_date: isoValue })
.eq("id", true);
if (!updateError) {
setTribunalDate(isoValue);
setEditingTribunalDate(false);
}
}
async function handleAddDay(event: React.FormEvent) {
event.preventDefault();
setError(null);
if (!newDate) {
setError("Choisis une date.");
return;
}
const supabase = createClient();
const { error: insertError } = await supabase.from("days").insert({
date: newDate,
god_name: newGod,
god_domain: newGodDomain,
});
if (insertError) {
setError(
insertError.message.includes("duplicate key")
? "Une journée existe déjà à cette date."
: "Une erreur est survenue.",
);
return;
}
setAddingDay(false);
setNewDate("");
setNewGod(PANTHEON_PRESETS[0].name);
setNewGodDomain(PANTHEON_PRESETS[0].domain);
setCustomGod(false);
refetch();
}
return (
<div className="flex flex-col gap-6">
<div className="marble-surface rounded-2xl border border-gold/40 px-6 py-5 text-center shadow-xl">
<p className="font-heading text-lg tracking-wide text-text-marble uppercase sm:text-xl">
{formatCountdown(tribunalDate)}
</p>
{tribunalDate && (
<p className="mt-1 font-serif text-sm text-text-mut italic">
{new Date(tribunalDate).toLocaleString("fr-FR", {
dateStyle: "full",
timeStyle: "short",
timeZone: "Europe/Paris",
})}
</p>
)}
{isJudge && (
<div className="mt-3">
{editingTribunalDate ? (
<div className="flex flex-wrap items-center justify-center gap-2">
<input
type="datetime-local"
value={tribunalInput}
onChange={(e) => setTribunalInput(e.target.value)}
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1 text-sm text-text-marble"
/>
<button
type="button"
onClick={handleSaveTribunalDate}
className="rounded-md bg-ink-2 px-3 py-1.5 text-xs font-medium text-gold-bright hover:bg-ink"
>
Enregistrer
</button>
<button
type="button"
onClick={() => setEditingTribunalDate(false)}
className="rounded-md border border-gold/30 px-3 py-1.5 text-xs text-text-marble hover:bg-gold/10"
>
Annuler
</button>
</div>
) : (
<button
type="button"
onClick={() => setEditingTribunalDate(true)}
className="rounded-md border border-gold/40 px-3 py-1.5 text-xs font-medium text-text-marble hover:bg-gold/10"
>
Définir la date du Tribunal
</button>
)}
</div>
)}
</div>
{days.length === 0 && !addingDay && (
<p className="text-center text-sm text-marble/60">
Aucune journée n&apos;a encore é inscrite au calendrier.
</p>
)}
<div className="flex flex-col gap-4">
{days.map((day) => (
<DayCard key={day.id} day={day} isJudge={isJudge} onChanged={refetch} />
))}
</div>
{isJudge && (
<div className="marble-surface rounded-2xl border border-gold/40 p-4 shadow-sm">
{addingDay ? (
<form onSubmit={handleAddDay} className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-marble">Date</label>
<input
type="date"
value={newDate}
onChange={(e) => setNewDate(e.target.value)}
required
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1.5 text-sm text-text-marble"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-marble">Divinité</label>
<select
value={customGod ? "custom" : newGod}
onChange={(e) => {
if (e.target.value === "custom") {
setCustomGod(true);
setNewGod("");
setNewGodDomain("");
} else {
setCustomGod(false);
setNewGod(e.target.value);
setNewGodDomain(getGodPreset(e.target.value)?.domain ?? "");
}
}}
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1.5 text-sm text-text-marble"
>
{PANTHEON_PRESETS.map((g) => (
<option key={g.name} value={g.name}>
{g.name}
</option>
))}
<option value="custom">Autre</option>
</select>
</div>
{customGod && (
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-marble">Nom de la divinité</label>
<input
type="text"
value={newGod}
onChange={(e) => setNewGod(e.target.value)}
required
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1.5 text-sm text-text-marble"
/>
</div>
)}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-text-marble">Domaine</label>
<input
type="text"
value={newGodDomain}
onChange={(e) => setNewGodDomain(e.target.value)}
required
className="rounded-md border border-gold/30 bg-white/50 px-2 py-1.5 text-sm text-text-marble"
/>
</div>
{error && <p className="text-sm text-oxblood">{error}</p>}
<div className="flex gap-2">
<button
type="submit"
className="rounded-md bg-ink-2 px-4 py-2 font-heading text-sm tracking-wide text-gold-bright uppercase hover:bg-ink"
>
Ajouter
</button>
<button
type="button"
onClick={() => setAddingDay(false)}
className="rounded-md border border-gold/30 px-4 py-2 text-sm text-text-marble hover:bg-gold/10"
>
Annuler
</button>
</div>
</form>
) : (
<button
type="button"
onClick={() => setAddingDay(true)}
className="flex w-full items-center justify-center gap-2 rounded-md border border-gold/40 px-4 py-2 text-sm font-medium text-text-marble hover:bg-gold/10"
>
<IconPlus className="h-4 w-4" />
Ajouter une journée
</button>
)}
</div>
)}
</div>
);
}