mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-23 15:18: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 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 { auth } from "@/auth";
|
||||||
import { db } from "@/db";
|
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 { SiteFooter } from "@/components/SiteFooter";
|
||||||
import { SiteHeader } from "@/components/SiteHeader";
|
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";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -45,6 +46,25 @@ export default async function AssociationPage() {
|
|||||||
.groupBy(meetings.id)
|
.groupBy(meetings.id)
|
||||||
.orderBy(asc(meetings.startsAt));
|
.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 =
|
const myRegistrations =
|
||||||
memberId && upcoming.length > 0
|
memberId && upcoming.length > 0
|
||||||
? await db
|
? await db
|
||||||
@@ -61,6 +81,14 @@ export default async function AssociationPage() {
|
|||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
const myMeetingIds = new Set(myRegistrations.map((row) => row.meetingId));
|
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 = [
|
const officers = [
|
||||||
["Vice-présidence", settings.association_vice_president],
|
["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 remaining = Math.max(0, meeting.capacity - registered);
|
||||||
const isRegistered = myMeetingIds.has(meeting.id);
|
const isRegistered = myMeetingIds.has(meeting.id);
|
||||||
return (
|
return (
|
||||||
<article id={`rencontre-${meeting.id}`} key={meeting.id} className="lift" style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 12, overflow: "hidden", scrollMarginTop: 100 }}>
|
<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={{ aspectRatio: "16 / 10", background: meeting.imageUrl ? `center/cover no-repeat url(${meeting.imageUrl})` : "repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)" }} />
|
<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={{ padding: 18 }}>
|
||||||
<div style={{ color: "#9a6638", fontSize: 12.5, fontWeight: 800 }}>
|
<div style={{ color: "#9a6638", fontSize: 12.5, fontWeight: 800 }}>
|
||||||
{formatDate(meeting.startsAt)} · {formatTime(meeting.startsAt)}
|
{formatDate(meeting.startsAt)} · {formatTime(meeting.startsAt)}
|
||||||
@@ -166,6 +200,50 @@ export default async function AssociationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 }}>
|
<section className="container" style={{ paddingTop: 54, paddingBottom: 64 }}>
|
||||||
<div style={{ color: "#9a6638", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 800 }}>
|
<div style={{ color: "#9a6638", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", fontWeight: 800 }}>
|
||||||
Gouvernance
|
Gouvernance
|
||||||
|
|||||||
@@ -218,7 +218,9 @@ export default async function EspacePage() {
|
|||||||
Droit à l'image
|
Droit à l'image
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ color: "#8c8068", fontSize: 13, marginTop: 4 }}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{latestConsent && (
|
{latestConsent && (
|
||||||
@@ -227,7 +229,12 @@ export default async function EspacePage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ImageConsentForm saveAction={saveImageConsent} defaultName={memberName} compact />
|
<ImageConsentForm
|
||||||
|
saveAction={saveImageConsent}
|
||||||
|
defaultName={memberName}
|
||||||
|
compact
|
||||||
|
currentDecision={latestConsent?.decision === "accepted" ? "accepted" : latestConsent?.decision === "refused" ? "refused" : null}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default async function PastMeetingsAdminPage() {
|
|||||||
<div style={{ display: "grid", gap: 28 }}>
|
<div style={{ display: "grid", gap: 28 }}>
|
||||||
<section style={panelStyle}>
|
<section style={panelStyle}>
|
||||||
<h2 className="font-display" style={{ margin: "0 0 6px", fontSize: 20, color: "#26201a" }}>Nouvelle rencontre passée</h2>
|
<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 }}>
|
<form action={createPastMeeting} className="grid grid-2" style={{ gap: 16 }}>
|
||||||
<div>
|
<div>
|
||||||
<label className="field-label">Titre</label>
|
<label className="field-label">Titre</label>
|
||||||
|
|||||||
@@ -26,18 +26,22 @@ export default async function PastMeetingsPage() {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const photosByPast = new Map<number, typeof photos>();
|
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>();
|
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 (
|
return (
|
||||||
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
||||||
<SiteHeader active="past" logo />
|
<SiteHeader active="association" logo />
|
||||||
<main>
|
<main>
|
||||||
<section style={{ background: "#13324F", color: "#fff" }}>
|
<section style={{ background: "#13324F", color: "#fff" }}>
|
||||||
<div className="container" style={{ paddingTop: 58, paddingBottom: 54 }}>
|
<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>
|
<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 }}>
|
<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.
|
Revivez en images les temps forts de Plein R et les moments partagés par les acteurs économiques du Bassin de Pompey.
|
||||||
</p>
|
</p>
|
||||||
@@ -50,6 +54,7 @@ export default async function PastMeetingsPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-3" style={{ gap: 20 }}>
|
<div className="grid grid-3" style={{ gap: 20 }}>
|
||||||
{archives.map((archive) => {
|
{archives.map((archive) => {
|
||||||
|
const archivePhotos = photosByPast.get(archive.id) ?? [];
|
||||||
const participantCount = archive.meetingId
|
const participantCount = archive.meetingId
|
||||||
? registrationCounts.get(archive.meetingId) ?? 0
|
? registrationCounts.get(archive.meetingId) ?? 0
|
||||||
: splitLines(archive.participants ?? "").length;
|
: 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.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>}
|
{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 }}>
|
<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>
|
</div>
|
||||||
<PastMeetingGallery title={archive.title} photos={photosByPast.get(archive.id) ?? []} />
|
<PastMeetingGallery title={archive.title} photos={archivePhotos} />
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ export function ImageConsentForm({
|
|||||||
saveAction,
|
saveAction,
|
||||||
defaultName,
|
defaultName,
|
||||||
compact = false,
|
compact = false,
|
||||||
|
currentDecision = null,
|
||||||
}: {
|
}: {
|
||||||
saveAction: (formData: FormData) => Promise<void>;
|
saveAction: (formData: FormData) => Promise<void>;
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
currentDecision?: "accepted" | "refused" | null;
|
||||||
}) {
|
}) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const [drawing, setDrawing] = useState(false);
|
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 (
|
return (
|
||||||
<form
|
<form
|
||||||
action={saveAction}
|
action={saveAction}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const NAV = [
|
|||||||
{ key: "annuaire", label: "Annuaire", href: "/annuaire" },
|
{ key: "annuaire", label: "Annuaire", href: "/annuaire" },
|
||||||
{ key: "promotions", label: "Promotions", href: "/#promotions" },
|
{ key: "promotions", label: "Promotions", href: "/#promotions" },
|
||||||
{ key: "association", label: "L'association", href: "/association" },
|
{ 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 }) {
|
export function SiteHeader({ active, logo = false }: { active?: string; logo?: boolean }) {
|
||||||
|
|||||||
Reference in new issue
Block a user