mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-12 01:38:04 +02:00
polish meeting and consent views
This commit is contained in:
1 parent
afc805c96a
commit
5e7233f98d
6 files changed
+131
-15
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in new issue
Block a user