From 3a6273c5b45f7d39239d6c729c06a14fcd16e452 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 21:06:02 +0000 Subject: [PATCH] =?UTF-8?q?Ajoute=20la=20page=20fiche=20adh=C3=A9rent=20pu?= =?UTF-8?q?blique=20(/adherents/[id])?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profil public d'un commerçant : vitrine, catégorie, localisation, description, bouton de contact et promotions en cours. Réservée aux adhérents actifs (404 sinon). Les cartes « À l'honneur » de l'accueil pointent désormais vers la fiche. Nouvelles requêtes getPublicMember / getMemberLivePromotions. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL --- src/app/adherents/[id]/page.tsx | 332 ++++++++++++++++++++++++++++++++ src/app/page.tsx | 7 +- src/lib/queries.ts | 37 ++++ 3 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 src/app/adherents/[id]/page.tsx diff --git a/src/app/adherents/[id]/page.tsx b/src/app/adherents/[id]/page.tsx new file mode 100644 index 0000000..ca4d5a1 --- /dev/null +++ b/src/app/adherents/[id]/page.tsx @@ -0,0 +1,332 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import { Sparkle } from "@/components/Sparkle"; +import { getMemberLivePromotions, getPublicMember } from "@/lib/queries"; + +export const dynamic = "force-dynamic"; + +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 async function generateMetadata({ + params, +}: { + params: Promise<{ id: string }>; +}): Promise { + const { id } = await params; + const member = await getPublicMember(Number(id)); + if (!member) return { title: "Adhérent — Plein R" }; + return { + title: `${member.name} — Plein R`, + description: member.description ?? undefined, + }; +} + +export default async function FicheAdherentPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const memberId = Number(id); + if (!memberId) notFound(); + + const member = await getPublicMember(memberId); + if (!member || member.status !== "active") notFound(); + + const promos = await getMemberLivePromotions(memberId); + const accent = member.accent ?? "#E0A63C"; + const location = [member.address, member.city].filter(Boolean).join(" · "); + + return ( +
+ {/* header */} +
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + Plein R + + Plein R + + + +
+ +
+
+ + ← Retour à l'annuaire + +
+ + {/* hero card */} +
+ + + + {/* vitrine photo */} +
+ {!member.logoUrl && ( + + photo vitrine + + )} +
+ + {/* info */} +
+ {member.categoryLabel && ( + + {member.categoryLabel} + + )} +

+ {member.name} +

+ {location && ( +
+ + {location} +
+ )} + {member.description && ( +

+ {member.description} +

+ )} +
+ {member.email && ( + + Contacter + + )} + {promos.length > 0 && ( + + Voir les promotions + + )} +
+
+
+ + {/* promotions */} +
+

+ Promotions en cours +

+ + {promos.length === 0 ? ( +
+ Cet adhérent n'a pas de promotion en cours pour le moment. +
+ ) : ( +
+ {promos.map((p) => ( +
+
+ {!p.imageUrl && ( + + photo de l'offre + + )} + {p.badge && ( + + {p.badge} + + )} + {p.category && ( + + {p.category} + + )} +
+
+

+ {p.title} +

+

+ {p.text} +

+ {p.validUntil && ( +
+ {p.validUntil} +
+ )} +
+
+ ))} +
+ )} +
+
+ + {/* footer */} +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Plein R + + Plein R + +
+
+ contact@plein-r.fr · Bassin de Pompey +
+ Réseau · Rencontre · Réussite +
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 89321ca..151d821 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -470,10 +470,11 @@ export default async function AccueilPage() {
{highlighted.map((m, i) => ( -
-
+ ))}
diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 94be7d2..0be7353 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -44,6 +44,43 @@ export async function getLivePromotions(limit = 6) { return rows; } +export async function getPublicMember(id: number) { + const [m] = await db + .select({ + id: members.id, + name: members.name, + email: members.email, + description: members.description, + city: members.city, + address: members.address, + status: members.status, + logoUrl: members.logoUrl, + categoryLabel: categories.label, + accent: categories.accent, + tint: categories.tint, + }) + .from(members) + .leftJoin(categories, eq(members.categoryId, categories.id)) + .where(eq(members.id, id)); + return m ?? null; +} + +export async function getMemberLivePromotions(memberId: number) { + return db + .select({ + id: promotions.id, + title: promotions.title, + text: promotions.text, + category: promotions.category, + badge: promotions.badge, + imageUrl: promotions.imageUrl, + validUntil: promotions.validUntil, + }) + .from(promotions) + .where(and(eq(promotions.memberId, memberId), eq(promotions.status, "live"))) + .orderBy(desc(promotions.createdAt)); +} + export async function getHighlightedMembers(limit = 3) { const rows = await db .select({