Promotions : page publique dédiée, l'accueil garde les six dernières

`/promotions` liste toutes les offres en cours ; l'accueil en montre six et
renvoie vers elle. Les deux listes partagent `PromoCard`, extrait de la page
d'accueil : un seul rendu de carte à maintenir.

- `getLivePromotions(limit)` accepte `null` pour « toutes les offres », la
  limite restant appliquée en SQL et non après coup.
- Page complète : `<h1>` unique, `<main>`, métadonnées et canonique via
  `pageMetadata`, JSON-LD `BreadcrumbList` + `ItemList`, entrée au sitemap,
  état vide qui renvoie vers l'annuaire.
- Les liens « Promotions » de l'en-tête, du pied de page et de l'espace
  adhérent visent désormais cette page. Idem pour le lien d'une publication
  réseau sans adhérent rattaché, qui pointait sur l'ancre `/#promotions`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
Claude committed 2026-09-06 14:34:27 +00:00
1 parent cf8cf5c8e2
commit a52e13e623
10 files changed
+294 -116

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
/**
* Carte d'une promotion sur le site public.
*
* Un seul rendu pour l'accueil (les 6 dernières) et la page `/promotions`
* (toutes les offres en cours) : les deux listes doivent rester identiques.
*/
import Link from "next/link";
import { PromoImage } from "@/components/PromoImage";
import { MemberAvatar } from "@/components/MemberAvatar";
import { formatValidity } from "@/lib/promo-validity";
import { memberPath } from "@/lib/seo";
const STRIPE_WARM =
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
/** Rouge pour une remise chiffrée, vert pour une offre en nature. */
function badgeColor(badge: string | null) {
if (!badge) return "#1f8a5b";
return badge.includes("%") ? "#d8472b" : "#1f8a5b";
}
export type PublicPromo = {
id: number;
title: string;
text: string | null;
category: string | null;
badge: string | null;
imageUrl: string | null;
validUntil: string | null;
startsOn: string | null;
endsOn: string | null;
memberId: number | null;
memberName: string | null;
memberCity: string | null;
memberLogoUrl: string | null;
};
export function PromoCard({ promo: p }: { promo: PublicPromo }) {
const validity = formatValidity(p);
return (
<Link
href={p.memberId ? memberPath({ id: p.memberId, name: p.memberName ?? "", city: p.memberCity }) : "#"}
className="lift-card"
style={{
textDecoration: "none",
color: "inherit",
background: "#fff",
border: "1px solid #ece3d0",
borderRadius: 18,
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
position: "relative",
aspectRatio: "1 / 1",
background: STRIPE_WARM,
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{p.imageUrl && <PromoImage src={p.imageUrl} alt={p.title ?? ""} />}
{!p.imageUrl && (
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
photo de l&apos;offre
</span>
)}
{p.badge && (
<span
className="font-display"
style={{
position: "absolute",
top: 0,
right: 0,
background: badgeColor(p.badge),
color: "#fff",
fontWeight: 800,
fontSize: 18,
padding: "8px 16px",
borderRadius: "0 0 0 16px",
}}
>
{p.badge}
</span>
)}
{p.category && (
<span
style={{
position: "absolute",
top: 12,
left: 12,
background: "#9a6638",
color: "#fff",
borderRadius: 999,
padding: "5px 12px",
fontSize: 11,
fontWeight: 700,
}}
>
{p.category}
</span>
)}
</div>
<div style={{ padding: "17px 18px 18px", display: "flex", flexDirection: "column", flex: 1 }}>
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 18, margin: "0 0 5px", color: "#26201a" }}>
{p.title}
</h3>
<p style={{ margin: "0 0 14px", fontSize: 13.5, color: "#8c8068", lineHeight: 1.5, flex: 1 }}>
{p.text}
</p>
<div style={{ borderTop: "1px solid #f0e8d6", paddingTop: 12 }}>
{/* Le logo du commerce juste à gauche de son nom. */}
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
<MemberAvatar name={p.memberName ?? ""} logoUrl={p.memberLogoUrl} size={28} />
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#3c3322" }}>{p.memberName}</div>
{validity && <div style={{ fontSize: 11.5, color: "#a99c82" }}>{validity}</div>}
</div>
</div>
</div>
</div>
</Link>
);
}
+1 -1
View File
@@ -32,7 +32,7 @@ export async function SiteFooter() {
<div className="footer-title">Accès rapide</div>
<div className="footer-links">
<Link href="/annuaire">Annuaire</Link>
<Link href="/#promotions">Promotions</Link>
<Link href="/promotions">Promotions</Link>
<Link href="/association">L&apos;association</Link>
<Link href="/rencontres-passees">Rencontres passées</Link>
<Link href="/login">Espace adhérent</Link>
+1 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
const NAV = [
{ key: "accueil", label: "Accueil", href: "/" },
{ 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" },
];