polish meeting and consent views

This commit is contained in:
Michael committed 2026-07-18 04:17:28 +02:00
1 parent afc805c96a
commit 5e7233f98d
6 files changed
+131 -15

No files matched your search

+83 -5
View File
@@ -1,11 +1,12 @@
import Link from "next/link";
import { and, asc, eq, gte, inArray, sql } from "drizzle-orm";
import { and, asc, desc, eq, gte, inArray, sql } from "drizzle-orm";
import { auth } from "@/auth";
import { db } from "@/db";
import { meetingRegistrations, meetings } from "@/db/schema";
import { meetingRegistrations, meetings, pastMeetingPhotos, pastMeetings } from "@/db/schema";
import { PastMeetingGallery } from "@/components/PastMeetingGallery";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { getSiteSettings, parseBoardMembers } from "@/lib/site-settings";
import { getSiteSettings, parseBoardMembers, splitLines } from "@/lib/site-settings";
export const dynamic = "force-dynamic";
@@ -45,6 +46,25 @@ export default async function AssociationPage() {
.groupBy(meetings.id)
.orderBy(asc(meetings.startsAt));
const past = await db.select().from(pastMeetings).orderBy(desc(pastMeetings.eventDate)).limit(3);
const pastIds = past.map((meeting) => meeting.id);
const linkedMeetingIds = past.map((meeting) => meeting.meetingId).filter((id): id is number => id !== null);
const [pastPhotos, linkedRegistrations] = await Promise.all([
pastIds.length > 0
? db
.select()
.from(pastMeetingPhotos)
.where(inArray(pastMeetingPhotos.pastMeetingId, pastIds))
.orderBy(asc(pastMeetingPhotos.position), asc(pastMeetingPhotos.id))
: [],
linkedMeetingIds.length > 0
? db
.select({ meetingId: meetingRegistrations.meetingId, id: meetingRegistrations.id })
.from(meetingRegistrations)
.where(inArray(meetingRegistrations.meetingId, linkedMeetingIds))
: [],
]);
const myRegistrations =
memberId && upcoming.length > 0
? await db
@@ -61,6 +81,14 @@ export default async function AssociationPage() {
)
: [];
const myMeetingIds = new Set(myRegistrations.map((row) => row.meetingId));
const photosByPast = new Map<number, typeof pastPhotos>();
for (const photo of pastPhotos) {
photosByPast.set(photo.pastMeetingId, [...(photosByPast.get(photo.pastMeetingId) ?? []), photo]);
}
const registrationCounts = new Map<number, number>();
for (const registration of linkedRegistrations) {
registrationCounts.set(registration.meetingId, (registrationCounts.get(registration.meetingId) ?? 0) + 1);
}
const officers = [
["Vice-présidence", settings.association_vice_president],
@@ -133,8 +161,14 @@ export default async function AssociationPage() {
const remaining = Math.max(0, meeting.capacity - registered);
const isRegistered = myMeetingIds.has(meeting.id);
return (
<article id={`rencontre-${meeting.id}`} key={meeting.id} className="lift" style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 12, overflow: "hidden", scrollMarginTop: 100 }}>
<div style={{ aspectRatio: "16 / 10", background: meeting.imageUrl ? `center/cover no-repeat url(${meeting.imageUrl})` : "repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)" }} />
<article id={`rencontre-${meeting.id}`} key={meeting.id} className="lift" style={{ background: "#fff", border: isRegistered ? "2px solid #1f8a5b" : "1px solid #e6dcc6", borderRadius: 12, overflow: "hidden", scrollMarginTop: 100 }}>
<div style={{ position: "relative", aspectRatio: "16 / 10", background: meeting.imageUrl ? `center/cover no-repeat url(${meeting.imageUrl})` : "repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)" }}>
{isRegistered && (
<span style={{ position: "absolute", top: 12, right: 12, display: "inline-flex", alignItems: "center", gap: 6, background: "#1f8a5b", color: "#fff", borderRadius: 999, padding: "7px 12px", fontSize: 12, fontWeight: 800, boxShadow: "0 4px 14px rgba(22,72,49,0.28)" }}>
✓ Inscrit
</span>
)}
</div>
<div style={{ padding: 18 }}>
<div style={{ color: "#9a6638", fontSize: 12.5, fontWeight: 800 }}>
{formatDate(meeting.startsAt)} · {formatTime(meeting.startsAt)}
@@ -166,6 +200,50 @@ export default async function AssociationPage() {
</div>
</section>
<section style={{ background: "#EFE9DA" }}>
<div className="container" style={{ paddingTop: 54, paddingBottom: 54 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "end", gap: 18, flexWrap: "wrap", marginBottom: 20 }}>
<div>
<div style={{ color: "#9a6638", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 800 }}>
Ils y étaient
</div>
<h2 className="font-display" style={{ margin: "8px 0 0", color: "#26201a", fontSize: 34 }}>
Nos dernières rencontres
</h2>
</div>
<Link href="/rencontres-passees" style={{ display: "inline-block", background: "#13324F", color: "#fff", borderRadius: 8, padding: "11px 16px", fontSize: 13.5, fontWeight: 800, textDecoration: "none" }}>
Voir toutes les rencontres passées
</Link>
</div>
{past.length === 0 ? (
<div style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 8, padding: 24, color: "#8c8068" }}>
Aucune rencontre passée publiée pour le moment.
</div>
) : (
<div className="grid grid-3" style={{ gap: 18 }}>
{past.map((meeting) => {
const photos = photosByPast.get(meeting.id) ?? [];
const participantCount = meeting.meetingId
? registrationCounts.get(meeting.meetingId) ?? 0
: splitLines(meeting.participants ?? "").length;
return (
<article key={meeting.id} style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 8, padding: 18 }}>
<div style={{ color: "#9a6638", fontSize: 12.5, fontWeight: 800 }}>{formatDate(meeting.eventDate)}</div>
<h3 className="font-display" style={{ color: "#26201a", fontSize: 22, margin: "7px 0" }}>{meeting.title}</h3>
{meeting.location && <div style={{ color: "#6c6150", fontSize: 13, fontWeight: 700 }}>{meeting.location}</div>}
{meeting.description && <p style={{ color: "#8c8068", fontSize: 13.5, lineHeight: 1.65 }}>{meeting.description}</p>}
<div style={{ color: "#6c6150", fontSize: 12.5, fontWeight: 800, marginBottom: 12 }}>
{participantCount} participant{participantCount > 1 ? "s" : ""} · {photos.length} photo{photos.length > 1 ? "s" : ""}
</div>
<PastMeetingGallery title={meeting.title} photos={photos} />
</article>
);
})}
</div>
)}
</div>
</section>
<section className="container" style={{ paddingTop: 54, paddingBottom: 64 }}>
<div style={{ color: "#9a6638", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 800 }}>
Gouvernance
+9 -2
View File
@@ -218,7 +218,9 @@ export default async function EspacePage() {
Droit à l'image
</h3>
<div style={{ color: "#8c8068", fontSize: 13, marginTop: 4 }}>
Vous pouvez modifier votre décision à tout moment.
{latestConsent?.decision === "accepted"
? "Autorisation signée et enregistrée. Vous pouvez la retirer à tout moment."
: "Vous pouvez autoriser l'utilisation de votre image à tout moment."}
</div>
</div>
{latestConsent && (
@@ -227,7 +229,12 @@ export default async function EspacePage() {
</span>
)}
</div>
<ImageConsentForm saveAction={saveImageConsent} defaultName={memberName} compact />
<ImageConsentForm
saveAction={saveImageConsent}
defaultName={memberName}
compact
currentDecision={latestConsent?.decision === "accepted" ? "accepted" : latestConsent?.decision === "refused" ? "refused" : null}
/>
</section>
)}
+1 -1
View File
@@ -56,7 +56,7 @@ export default async function PastMeetingsAdminPage() {
<div style={{ display: "grid", gap: 28 }}>
<section style={panelStyle}>
<h2 className="font-display" style={{ margin: "0 0 6px", fontSize: 20, color: "#26201a" }}>Nouvelle rencontre passée</h2>
<p style={{ color: "#8c8068", fontSize: 13, margin: "0 0 18px" }}>Les archives publiées apparaissent sur la page publique « Rencontres passées ».</p>
<p style={{ color: "#8c8068", fontSize: 13, margin: "0 0 18px" }}>Les trois archives les plus récentes apparaissent sur « L'association » et la page complète rassemble toutes les rencontres passées.</p>
<form action={createPastMeeting} className="grid grid-2" style={{ gap: 16 }}>
<div>
<label className="field-label">Titre</label>
+11 -6
View File
@@ -26,18 +26,22 @@ export default async function PastMeetingsPage() {
: [];
const photosByPast = new Map<number, typeof photos>();
for (const photo of photos) photosByPast.set(photo.pastMeetingId, [...(photosByPast.get(photo.pastMeetingId) ?? []), photo]);
for (const photo of photos) {
photosByPast.set(photo.pastMeetingId, [...(photosByPast.get(photo.pastMeetingId) ?? []), photo]);
}
const registrationCounts = new Map<number, number>();
for (const registration of linkedRegistrations) registrationCounts.set(registration.meetingId, (registrationCounts.get(registration.meetingId) ?? 0) + 1);
for (const registration of linkedRegistrations) {
registrationCounts.set(registration.meetingId, (registrationCounts.get(registration.meetingId) ?? 0) + 1);
}
return (
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
<SiteHeader active="past" logo />
<SiteHeader active="association" logo />
<main>
<section style={{ background: "#13324F", color: "#fff" }}>
<div className="container" style={{ paddingTop: 58, paddingBottom: 54 }}>
<div style={{ color: "#E0A63C", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 800 }}>Ils y étaient</div>
<h1 className="font-display" style={{ margin: "10px 0 14px", fontSize: "clamp(38px, 7vw, 62px)", lineHeight: 1 }}>Nos rencontres passées</h1>
<h1 className="font-display" style={{ margin: "10px 0 14px", fontSize: "clamp(38px, 7vw, 62px)", lineHeight: 1 }}>Toutes nos rencontres passées</h1>
<p style={{ maxWidth: 680, color: "#cfe0ee", fontSize: 17, lineHeight: 1.7, margin: 0 }}>
Revivez en images les temps forts de Plein R et les moments partagés par les acteurs économiques du Bassin de Pompey.
</p>
@@ -50,6 +54,7 @@ export default async function PastMeetingsPage() {
) : (
<div className="grid grid-3" style={{ gap: 20 }}>
{archives.map((archive) => {
const archivePhotos = photosByPast.get(archive.id) ?? [];
const participantCount = archive.meetingId
? registrationCounts.get(archive.meetingId) ?? 0
: splitLines(archive.participants ?? "").length;
@@ -60,9 +65,9 @@ export default async function PastMeetingsPage() {
{archive.location && <div style={{ color: "#6c6150", fontSize: 13, fontWeight: 700 }}>{archive.location}</div>}
{archive.description && <p style={{ color: "#8c8068", fontSize: 13.5, lineHeight: 1.65 }}>{archive.description}</p>}
<div style={{ color: "#6c6150", fontSize: 12.5, fontWeight: 800, marginBottom: 12 }}>
{participantCount} participant{participantCount > 1 ? "s" : ""} · {(photosByPast.get(archive.id) ?? []).length} photo{(photosByPast.get(archive.id) ?? []).length > 1 ? "s" : ""}
{participantCount} participant{participantCount > 1 ? "s" : ""} · {archivePhotos.length} photo{archivePhotos.length > 1 ? "s" : ""}
</div>
<PastMeetingGallery title={archive.title} photos={photosByPast.get(archive.id) ?? []} />
<PastMeetingGallery title={archive.title} photos={archivePhotos} />
</article>
);
})}
+27
View File
@@ -6,10 +6,12 @@ export function ImageConsentForm({
saveAction,
defaultName,
compact = false,
currentDecision = null,
}: {
saveAction: (formData: FormData) => Promise<void>;
defaultName: string;
compact?: boolean;
currentDecision?: "accepted" | "refused" | null;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [drawing, setDrawing] = useState(false);
@@ -65,6 +67,31 @@ export function ImageConsentForm({
}
}
if (currentDecision === "accepted") {
return (
<form action={saveAction} style={{ marginTop: 12 }}>
<input type="hidden" name="signatoryName" value={defaultName} />
<input type="hidden" name="signaturePng" value="" />
<button
type="submit"
name="decision"
value="refused"
style={{
border: "1px solid #e0c3bb",
background: "#fff",
color: "#d8472b",
fontWeight: 800,
borderRadius: 8,
padding: "9px 14px",
cursor: "pointer",
}}
>
Retirer mon autorisation
</button>
</form>
);
}
return (
<form
action={saveAction}
-1
View File
@@ -5,7 +5,6 @@ const NAV = [
{ key: "annuaire", label: "Annuaire", href: "/annuaire" },
{ key: "promotions", label: "Promotions", href: "/#promotions" },
{ key: "association", label: "L'association", href: "/association" },
{ key: "past", label: "Rencontres passées", href: "/rencontres-passees" },
];
export function SiteHeader({ active, logo = false }: { active?: string; logo?: boolean }) {