Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/app/inscription/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Hand,
MapPin,
Ticket,
Users,
} from "lucide-react";
import Link from "next/link";

Expand Down Expand Up @@ -108,6 +109,7 @@ export default async function InscriptionPage({
);
// `trim()` : un espace collé depuis un traitement de texte est truthy, et
// afficherait un bouton « Réserver » qui ne mène nulle part.
const estReunion = event.kind === "meeting";
const billetterie = event.ticketingUrl?.trim() || null;
const hote = ticketingHostLabel(billetterie) ?? "la billetterie en ligne";
const aDesCreneaux = event.volunteerSlots.length > 0;
Expand Down Expand Up @@ -141,7 +143,12 @@ export default async function InscriptionPage({
permet au bandeau de billetterie de le déborder en -mx-6. */}
<div className="relative p-6 sm:p-8">
<span className="inline-flex items-center gap-2 rounded-lg bg-sea-200 px-3 py-2 text-xs font-extrabold uppercase tracking-[0.12em] text-brand-950">
{billetterie ? (
{estReunion ? (
<>
<Users className="h-4 w-4" />
Réunion de l’association
</>
) : billetterie ? (
<>
<Ticket className="h-4 w-4" />
{aDesCreneaux ? "Billetterie et bénévoles" : "Billetterie en ligne"}
Expand Down Expand Up @@ -252,7 +259,7 @@ export default async function InscriptionPage({
{/* Sans ce garde, un événement à billetterie seule afficherait
« 0 coup de main » à côté du bouton « Réserver » : le parent
lit « complet » et referme. */}
{aDesCreneaux && (
{aDesCreneaux && !estReunion && (
<div className="mt-7 flex items-center gap-3 rounded-2xl bg-brand-900 p-4">
<Hand
className="h-5 w-5 shrink-0 text-brand-300"
Expand All @@ -268,6 +275,36 @@ export default async function InscriptionPage({
</div>
</aside>

{estReunion ? (
<section className="rounded-2xl border-2 border-slate-200 bg-white p-6 sm:p-8">
<p className="text-xs font-extrabold uppercase tracking-[0.14em] text-brand-700">
Venir à la réunion
</p>
<h2 className="mt-2 text-2xl font-black tracking-[-0.035em] text-brand-950 sm:text-3xl">
Les réunions sont ouvertes aux parents
</h2>
<p className="mt-2 text-sm font-medium leading-6 text-slate-600">
C’est là qu’on décide de ce que fait l’association. Venez
écouter, ou prendre la parole : les deux se valent.
</p>
{/* Aucune liste de présents ici : qui vient à une réunion est une
information réservée aux membres connectés. */}
<div className="mt-6 flex flex-wrap gap-3">
<Link
href="/contact"
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-xl bg-brand-950 px-5 py-3 text-sm font-extrabold text-white transition-colors hover:bg-brand-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-brand-200"
>
Poser une question
</Link>
<Link
href="/rejoindre"
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-xl border-2 border-slate-200 px-5 py-3 text-sm font-extrabold text-brand-800 transition-colors hover:border-brand-300"
>
Rejoindre l’association
</Link>
</div>
</section>
) : (
<section className="rounded-2xl border-2 border-slate-200 bg-white p-6 sm:p-8">
<p className="text-xs font-extrabold uppercase tracking-[0.14em] text-brand-700">
Aider à l’organisation
Expand Down Expand Up @@ -320,6 +357,7 @@ export default async function InscriptionPage({
/>
)}
</section>
)}
</div>
</main>

Expand Down
15 changes: 12 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MessageCircle,
MousePointerClick,
Ticket,
Users,
} from "lucide-react";
import Link from "next/link";

Expand Down Expand Up @@ -249,6 +250,12 @@ export default async function HomePage() {
lien, une ancre imbriquée serait invalide — et
personne ne doit partir payer depuis l'accueil
sans avoir lu la date ni le lieu. */}
{event.kind === "meeting" && (
<span className="inline-flex items-center gap-1.5 rounded-lg bg-brand-950 px-2.5 py-1.5 text-xs font-extrabold text-white">
<Users className="h-3.5 w-3.5" />
Réunion
</span>
)}
{event.ticketingUrl && (
<span className="inline-flex items-center gap-1.5 rounded-lg bg-brand-100 px-2.5 py-1.5 text-xs font-extrabold text-brand-900">
<Ticket className="h-3.5 w-3.5" />
Expand Down Expand Up @@ -281,9 +288,11 @@ export default async function HomePage() {
/>
)}
<div className="mt-auto flex items-center gap-2 pt-6 text-sm font-extrabold text-brand-700">
{remaining > 0 && !event.ticketingUrl
? "Voir et se proposer"
: "Voir l’événement"}
{event.kind === "meeting"
? "Voir la réunion"
: remaining > 0 && !event.ticketingUrl
? "Voir et se proposer"
: "Voir l’événement"}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</div>
</div>
Expand Down
11 changes: 9 additions & 2 deletions src/app/rejoindre/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export default async function RejoindrePage() {
const prochains = events.slice(0, 3).map((event, index) => ({
id: event.id,
titre: event.title,
reunion: event.kind === "meeting",
date: formatDateTime(event.startAt),
lieu: event.location,
jeton: event.shareToken,
Expand Down Expand Up @@ -497,15 +498,21 @@ export default async function RejoindrePage() {
</p>
)}
<div className="flex w-full items-center gap-3 sm:ml-auto sm:w-auto">
{event.restantes > 0 && (
{event.reunion && (
<span className="inline-flex items-center gap-1.5 rounded-lg bg-brand-950 px-2.5 py-1.5 text-xs font-extrabold text-white">
<Users className="h-3.5 w-3.5" />
Réunion
</span>
)}
{event.restantes > 0 && !event.reunion && (
<span className="inline-flex items-center gap-1.5 rounded-lg bg-sea-200 px-2.5 py-1.5 text-xs font-extrabold text-brand-950">
<Hand className="h-3.5 w-3.5" />
{event.restantes} place
{event.restantes > 1 ? "s" : ""}
</span>
)}
<span className="flex items-center gap-2 text-sm font-extrabold text-brand-700">
{event.restantes > 0
{event.restantes > 0 && !event.reunion
? "Se proposer"
: "Voir le rendez-vous"}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
Expand Down
35 changes: 22 additions & 13 deletions src/components/event-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,7 @@ export function EventForm({
<FormSection
number="1"
title="Informations essentielles"
description={
estReunion
? "Une réunion reste entre membres : elle n’apparaît jamais sur le site public."
: "Le titre est visible de tous, y compris des visiteurs du site."
}
description="Le titre est visible de tous, y compris des visiteurs du site."
>
<div>
<Label htmlFor="kind">De quoi s’agit-il ?</Label>
Expand All @@ -136,7 +132,7 @@ export function EventForm({
icone: Users,
titre: "Une réunion",
texte:
"Bureau, conseil, assemblée générale. Visible des seuls membres, chacun annonce s’il sera présent.",
"Bureau, conseil, assemblée générale. Annoncée sur le site comme les autres rendez-vous, et chaque membre indique s’il sera présent.",
},
].map(({ valeur, icone: Icone, titre, texte }) => (
<label
Expand Down Expand Up @@ -201,17 +197,17 @@ export function EventForm({
placeholder="Organisation interne, contacts, consignes…"
/>
</div>
<div className={estReunion ? "hidden" : undefined}>
<div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 shrink-0 text-slate-500" aria-hidden="true" />
<Label htmlFor="publicDescription" className="!mb-0">
Description publique
</Label>
</div>
<p className="mb-2 mt-1 text-xs leading-5 text-slate-500">
Affichée sur l’accueil du site et sur la page d’inscription des
bénévoles, dès que l’événement est publié. Ne rien y mettre que
vous ne diriez pas devant l’école.
{estReunion
? "Affichée sur le site dès que la réunion est publiée : l’ordre du jour tel que vous l’annonceriez aux familles. Vos notes internes, la préparation et les présences n’en sortent jamais."
: "Affichée sur l’accueil du site et sur la page d’inscription des bénévoles, dès que l’événement est publié. Ne rien y mettre que vous ne diriez pas devant l’école."}
</p>
<RichTextEditor
id="publicDescription"
Expand All @@ -234,8 +230,15 @@ export function EventForm({
id="location"
name="location"
defaultValue={event?.location ?? ""}
placeholder="Ex. Cour de l’école"
placeholder={estReunion ? "Ex. Salle des associations" : "Ex. Cour de l’école"}
/>
{estReunion && (
<p className="mt-2 text-xs leading-5 text-coral-700">
Une réunion publiée affiche son lieu sur le site. Si vous vous
retrouvez chez quelqu’un, écrivez plutôt « chez un parent, adresse
communiquée aux inscrits » que l’adresse elle-même.
</p>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
Expand Down Expand Up @@ -319,7 +322,11 @@ export function EventForm({
<FormSection
number="4"
title="Visibilité"
description="Vous pouvez tout préparer en brouillon, puis publier lorsque le lien doit être accessible."
description={
estReunion
? "Une réunion en brouillon reste entre membres. Publiée, sa date, son lieu et sa description publique apparaissent sur le site."
: "Vous pouvez tout préparer en brouillon, puis publier lorsque le lien doit être accessible."
}
>
<div>
<Label htmlFor="status">Qui peut voir l’événement ?</Label>
Expand All @@ -330,7 +337,9 @@ export function EventForm({
>
<option value="draft">Équipe uniquement — brouillon</option>
<option value="published">
Tout le monde — événement publié et inscriptions ouvertes
{estReunion
? "Tout le monde — réunion annoncée sur le site"
: "Tout le monde — événement publié et inscriptions ouvertes"}
</option>
<option value="archived">Archivé — conservé mais masqué</option>
</Select>
Expand Down
25 changes: 12 additions & 13 deletions src/lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ import {
} from "@/lib/db/schema";

/**
* Événements publiés à venir — pour la page d'accueil publique.
* Rendez-vous publiés à venir — pour les pages publiques. Réunions comprises :
* l'association veut que son agenda complet soit lisible des familles.
*
* Le filtre `kind = 'event'` est le premier des deux verrous qui tiennent les
* réunions hors du site public ; le second est dans getEventByShareToken.
* Ne jamais le retirer « pour simplifier » : une réunion de bureau publiée
* afficherait son ordre du jour et l'adresse d'un domicile sur Internet.
* Ce qui protège une réunion n'est donc plus son type mais son STATUT : tant
* qu'elle est en brouillon, elle reste interne. Ce que les pages publiques
* affichent d'un rendez-vous se limite au titre, à la date, au lieu et à la
* description publique ; les notes internes, la check-list et les présences
* n'en sortent jamais (voir les composants publics).
*/
export async function getUpcomingPublishedEvents() {
return db.query.events.findMany({
where: and(
eq(events.kind, "event"),
eq(events.status, "published"),
gte(events.startAt, new Date()),
),
Expand Down Expand Up @@ -99,17 +100,15 @@ export const getEventWithDetails = cache(async (id: string) => {
});

/**
* Un événement via son jeton public — pour l'inscription des bénévoles.
* `cache()` évite le double appel (generateMetadata + corps de la page).
* Un rendez-vous via son jeton public — page publique d'un événement ou d'une
* réunion. `cache()` évite le double appel (generateMetadata + corps de page).
*
* Second verrou : une réunion possède un jeton de partage comme tout
* événement, mais ce jeton ne doit ouvrir aucune page. Le filtre est porté par
* la requête et non par la page, pour qu'un futur appelant hérite de la
* protection sans avoir à y penser.
* La page n'affiche que ce qui est publiable ; en particulier elle ne charge
* pas les présences, qui sont une donnée de membres.
*/
export const getEventByShareToken = cache(async (token: string) => {
return db.query.events.findFirst({
where: and(eq(events.kind, "event"), eq(events.shareToken, token)),
where: eq(events.shareToken, token),
with: {
volunteerSlots: {
// Heure de début d'abord : un tableau de créneaux se lit dans l'ordre
Expand Down
Loading