mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-24 15:48:05 +02:00
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:
10 files changed
+294
-116
No files matched your search
@@ -68,6 +68,14 @@ ne sert plus que de repli d'affichage. La même phrase part dans le message
|
|||||||
Facebook / LinkedIn (`buildPromoMessage`) : un seul formateur pour le site,
|
Facebook / LinkedIn (`buildPromoMessage`) : un seul formateur pour le site,
|
||||||
le backoffice et les réseaux.
|
le backoffice et les réseaux.
|
||||||
|
|
||||||
|
La liste publique vit sur **`/promotions`** (toutes les offres en cours) ;
|
||||||
|
l'accueil n'en montre que les six dernières et renvoie vers elle. Les deux
|
||||||
|
listes partagent `src/components/PromoCard.tsx` : un seul rendu, pas deux
|
||||||
|
cartes à maintenir. `getLivePromotions(limit)` accepte `null` pour « tout »,
|
||||||
|
la limite restant appliquée en SQL. Les liens « Promotions » de l'en-tête, du
|
||||||
|
pied de page et des publications réseaux pointent sur cette page, plus sur
|
||||||
|
l'ancre `/#promotions`.
|
||||||
|
|
||||||
Une promotion n'est **affichée que pendant sa période** : les lectures de
|
Une promotion n'est **affichée que pendant sa période** : les lectures de
|
||||||
`src/lib/queries.ts` passent par `VISIBLE_PROMO` (statut `live` **et** fenêtre
|
`src/lib/queries.ts` passent par `VISIBLE_PROMO` (statut `live` **et** fenêtre
|
||||||
de dates, journée calculée en `Europe/Paris` — un conteneur en UTC retirerait
|
de dates, journée calculée en `Europe/Paris` — un conteneur en UTC retirerait
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export default async function EspacePromotionsPage() {
|
|||||||
|
|
||||||
{myPromos.length > 0 && (
|
{myPromos.length > 0 && (
|
||||||
<p style={{ fontSize: 13, color: "#8c8068", marginTop: 18 }}>
|
<p style={{ fontSize: 13, color: "#8c8068", marginTop: 18 }}>
|
||||||
Les offres en ligne apparaissent sur <Link href="/#promotions" style={{ color: "#9a6638", fontWeight: 700 }}>la page d'accueil</Link>
|
Les offres en ligne apparaissent sur <Link href="/promotions" style={{ color: "#9a6638", fontWeight: 700 }}>la page des promotions</Link>
|
||||||
{publicPath ? (
|
{publicPath ? (
|
||||||
<>
|
<>
|
||||||
{" "}et sur <Link href={publicPath} style={{ color: "#9a6638", fontWeight: 700 }}>votre fiche</Link>
|
{" "}et sur <Link href={publicPath} style={{ color: "#9a6638", fontWeight: 700 }}>votre fiche</Link>
|
||||||
|
|||||||
+5
-108
@@ -7,13 +7,11 @@ import { SiteFooter } from "@/components/SiteFooter";
|
|||||||
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
||||||
import { MembersCarousel } from "@/components/MembersCarousel";
|
import { MembersCarousel } from "@/components/MembersCarousel";
|
||||||
import { StarField } from "@/components/StarField";
|
import { StarField } from "@/components/StarField";
|
||||||
import { PromoImage } from "@/components/PromoImage";
|
import { PromoCard } from "@/components/PromoCard";
|
||||||
import { MemberAvatar } from "@/components/MemberAvatar";
|
|
||||||
import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons";
|
import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons";
|
||||||
import { getActiveMemberCount, getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
import { getActiveMemberCount, getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
||||||
import { getSiteSettings, socialLinks } from "@/lib/site-settings";
|
import { getSiteSettings, socialLinks } from "@/lib/site-settings";
|
||||||
import { formatValidity } from "@/lib/promo-validity";
|
import { SITE_DESCRIPTION, SITE_TITLE, pageMetadata } from "@/lib/seo";
|
||||||
import { SITE_DESCRIPTION, SITE_TITLE, memberPath, pageMetadata } from "@/lib/seo";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -28,14 +26,6 @@ export const metadata: Metadata = {
|
|||||||
title: { absolute: SITE_TITLE },
|
title: { absolute: SITE_TITLE },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STRIPE_WARM =
|
|
||||||
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
|
|
||||||
|
|
||||||
function badgeColor(badge: string | null) {
|
|
||||||
if (!badge) return "#1f8a5b";
|
|
||||||
return badge.includes("%") ? "#d8472b" : "#1f8a5b";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function AccueilPage() {
|
export default async function AccueilPage() {
|
||||||
const [promos, rotatingMembers, settings, memberCount] = await Promise.all([
|
const [promos, rotatingMembers, settings, memberCount] = await Promise.all([
|
||||||
getLivePromotions(6),
|
getLivePromotions(6),
|
||||||
@@ -228,107 +218,14 @@ export default async function AccueilPage() {
|
|||||||
Offres, réductions et nouveautés publiées par les commerçants du réseau.
|
Offres, réductions et nouveautés publiées par les commerçants du réseau.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="#promotions" className="link-arrow" style={{ textDecoration: "none", color: "#9a6638", fontWeight: 700, fontSize: 14.5, whiteSpace: "nowrap" }}>
|
<Link href="/promotions" className="link-arrow" style={{ textDecoration: "none", color: "#9a6638", fontWeight: 700, fontSize: 14.5, whiteSpace: "nowrap" }}>
|
||||||
Toutes les promos →
|
Toutes les promos →
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Reveal className="grid grid-3 reveal-stagger" style={{ marginTop: 26 }}>
|
<Reveal className="grid grid-3 reveal-stagger" style={{ marginTop: 26 }}>
|
||||||
{promos.map((p) => (
|
{promos.map((p) => (
|
||||||
<Link
|
<PromoCard key={p.id} promo={p} />
|
||||||
key={p.id}
|
|
||||||
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'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>
|
|
||||||
{formatValidity(p) && (
|
|
||||||
<div style={{ fontSize: 11.5, color: "#a99c82" }}>{formatValidity(p)}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Sparkle } from "@/components/Sparkle";
|
||||||
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
|
import { PromoCard } from "@/components/PromoCard";
|
||||||
|
import { JsonLd } from "@/components/JsonLd";
|
||||||
|
import { getLivePromotions } from "@/lib/queries";
|
||||||
|
import { breadcrumbJsonLd, memberListJsonLd, pageMetadata } from "@/lib/seo";
|
||||||
|
import { publicBaseUrl } from "@/lib/seo-server";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export const metadata: Metadata = pageMetadata({
|
||||||
|
title: "Bons plans et promotions du Bassin de Pompey",
|
||||||
|
description:
|
||||||
|
"Toutes les offres en cours chez les commerçants, artisans et entreprises adhérents Plein R : " +
|
||||||
|
"réductions, nouveautés et bons plans à Pompey, Frouard, Liverdun, Champigneulles…",
|
||||||
|
path: "/promotions",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function PromotionsPage() {
|
||||||
|
// `null` : toutes les offres en cours, là où l'accueil n'en montre que six.
|
||||||
|
const [promos, baseUrl] = await Promise.all([getLivePromotions(null), publicBaseUrl()]);
|
||||||
|
|
||||||
|
// Une offre pointe vers la fiche de son commerçant : la liste structurée
|
||||||
|
// renvoie donc vers les adhérents, pas vers des URLs de promotion.
|
||||||
|
const listed = promos
|
||||||
|
.filter((p) => p.memberId != null)
|
||||||
|
.map((p) => ({ id: p.memberId as number, name: p.memberName ?? "", city: p.memberCity }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
||||||
|
<SiteHeader active="promotions" />
|
||||||
|
<JsonLd
|
||||||
|
data={[
|
||||||
|
breadcrumbJsonLd(baseUrl, [
|
||||||
|
{ name: "Accueil", path: "/" },
|
||||||
|
{ name: "Promotions", path: "/promotions" },
|
||||||
|
]),
|
||||||
|
memberListJsonLd(baseUrl, "Promotions en cours chez les adhérents Plein R", listed),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="container">
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
margin: "30px 0 10px",
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid #e6dcc6",
|
||||||
|
borderRadius: 24,
|
||||||
|
padding: "38px 36px 42px",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow: "0 26px 60px -40px rgba(40,30,15,0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sparkle color="#E0A63C" size={20} style={{ top: 28, right: 44 }} duration={3.4} />
|
||||||
|
<Sparkle color="#6FB0C6" size={13} style={{ top: 64, right: 96 }} duration={2.7} delay={0.5} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
background: "#fdeccd",
|
||||||
|
color: "#9a6638",
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: "0.12em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
fontWeight: 800,
|
||||||
|
padding: "6px 13px",
|
||||||
|
borderRadius: 999,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bons plans
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
className="font-display"
|
||||||
|
style={{ fontWeight: 800, fontSize: "clamp(24px, 5vw, 34px)", letterSpacing: "-0.02em", margin: 0, color: "#26201a" }}
|
||||||
|
>
|
||||||
|
Les promotions de nos adhérents
|
||||||
|
</h1>
|
||||||
|
<p style={{ margin: "8px 0 0", color: "#8c8068", fontSize: 15, maxWidth: 640, lineHeight: 1.6 }}>
|
||||||
|
Toutes les offres en cours chez les commerçants, artisans et entreprises du réseau.
|
||||||
|
Chaque promotion renvoie vers la fiche de son commerce.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{promos.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 26,
|
||||||
|
border: "1px dashed #d8cdb4",
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: "34px 20px",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#8c8068",
|
||||||
|
fontSize: 14.5,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aucune promotion en cours pour le moment.
|
||||||
|
<br />
|
||||||
|
<Link href="/annuaire" style={{ color: "#9a6638", fontWeight: 700 }}>
|
||||||
|
Parcourir l'annuaire des adhérents →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 22, fontSize: 13, color: "#a99c82", fontWeight: 600 }}>
|
||||||
|
{promos.length} offre{promos.length > 1 ? "s" : ""} en cours
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-3" style={{ marginTop: 14 }}>
|
||||||
|
{promos.map((p) => (
|
||||||
|
<PromoCard key={p.id} promo={p} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p style={{ margin: "22px 0 40px", fontSize: 14, color: "#8c8068" }}>
|
||||||
|
Vous êtes commerçant du Bassin de Pompey ?{" "}
|
||||||
|
<Link href="/association" style={{ color: "#9a6638", fontWeight: 700 }}>
|
||||||
|
Rejoignez Plein R
|
||||||
|
</Link>{" "}
|
||||||
|
pour publier vos offres ici.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<SiteFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export async function SiteFooter() {
|
|||||||
<div className="footer-title">Accès rapide</div>
|
<div className="footer-title">Accès rapide</div>
|
||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<Link href="/annuaire">Annuaire</Link>
|
<Link href="/annuaire">Annuaire</Link>
|
||||||
<Link href="/#promotions">Promotions</Link>
|
<Link href="/promotions">Promotions</Link>
|
||||||
<Link href="/association">L'association</Link>
|
<Link href="/association">L'association</Link>
|
||||||
<Link href="/rencontres-passees">Rencontres passées</Link>
|
<Link href="/rencontres-passees">Rencontres passées</Link>
|
||||||
<Link href="/login">Espace adhérent</Link>
|
<Link href="/login">Espace adhérent</Link>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
const NAV = [
|
const NAV = [
|
||||||
{ key: "accueil", label: "Accueil", href: "/" },
|
{ key: "accueil", label: "Accueil", href: "/" },
|
||||||
{ 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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -72,8 +72,14 @@ export async function getCategoriesWithCounts(limit = 6) {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLivePromotions(limit = 6) {
|
/**
|
||||||
const rows = await db
|
* Promotions visibles, de la plus récente à la plus ancienne.
|
||||||
|
*
|
||||||
|
* `limit` à `null` : toutes les offres en cours (page `/promotions`) ; sinon
|
||||||
|
* les N dernières (accueil).
|
||||||
|
*/
|
||||||
|
export async function getLivePromotions(limit: number | null = 6) {
|
||||||
|
const query = db
|
||||||
.select({
|
.select({
|
||||||
id: promotions.id,
|
id: promotions.id,
|
||||||
title: promotions.title,
|
title: promotions.title,
|
||||||
@@ -93,8 +99,10 @@ export async function getLivePromotions(limit = 6) {
|
|||||||
.leftJoin(members, eq(promotions.memberId, members.id))
|
.leftJoin(members, eq(promotions.memberId, members.id))
|
||||||
.where(VISIBLE_PROMO)
|
.where(VISIBLE_PROMO)
|
||||||
.orderBy(desc(promotions.createdAt))
|
.orderBy(desc(promotions.createdAt))
|
||||||
.limit(limit);
|
.$dynamic();
|
||||||
return rows;
|
// La limite reste dans le SQL : la page « toutes les promos » est la seule à
|
||||||
|
// tout demander, l'accueil ne rapatrie que ses six cartes.
|
||||||
|
return limit === null ? query : query.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPublicMember(id: number) {
|
export async function getPublicMember(id: number) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export const PRIVATE_PATHS = ["/backend", "/api", "/login", "/inscription"];
|
|||||||
export const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" | "yearly" }[] = [
|
export const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" | "yearly" }[] = [
|
||||||
{ path: "/", priority: 1, changeFrequency: "daily" },
|
{ path: "/", priority: 1, changeFrequency: "daily" },
|
||||||
{ path: "/annuaire", priority: 0.9, changeFrequency: "daily" },
|
{ path: "/annuaire", priority: 0.9, changeFrequency: "daily" },
|
||||||
|
{ path: "/promotions", priority: 0.9, changeFrequency: "daily" },
|
||||||
{ path: "/association", priority: 0.8, changeFrequency: "weekly" },
|
{ path: "/association", priority: 0.8, changeFrequency: "weekly" },
|
||||||
{ path: "/rencontres-passees", priority: 0.6, changeFrequency: "monthly" },
|
{ path: "/rencontres-passees", priority: 0.6, changeFrequency: "monthly" },
|
||||||
{ path: "/mentions-legales", priority: 0.2, changeFrequency: "yearly" },
|
{ path: "/mentions-legales", priority: 0.2, changeFrequency: "yearly" },
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ export async function promoLink(promo: PromoForSharing): Promise<string | null>
|
|||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
return promo.memberId
|
return promo.memberId
|
||||||
? `${base}${memberPath({ id: promo.memberId, name: promo.memberName ?? "", city: promo.memberCity })}`
|
? `${base}${memberPath({ id: promo.memberId, name: promo.memberName ?? "", city: promo.memberCity })}`
|
||||||
: `${base}/#promotions`;
|
: `${base}/promotions`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Texte du post, commun aux deux réseaux. */
|
/** Texte du post, commun aux deux réseaux. */
|
||||||
|
|||||||
Reference in new issue
Block a user