Accueil: logo centre, "A l'honneur" en carrousel auto

- Hero : logo centre verticalement (alignItems center)
- Titre "A l'honneur ce mois-ci" -> "A l'honneur"
- Remplace les 3 fiches "mise en avant" par un carrousel auto-defilant
  de tous les adherents actifs, en rotation equitable (l'ordre pivote
  d'un cran par heure). Pause au survol, respecte prefers-reduced-motion.
- Retire la coche "Mettre a l'honneur" du back-office (edition adherent)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-27 10:31:55 +02:00
1 parent eb6536ce8a
commit 64ab10dea1
6 files changed
+161 -68

No files matched your search

-1
View File
@@ -250,7 +250,6 @@ export async function updateMember(formData: FormData) {
tags: String(formData.get("tags") ?? "").trim() || null,
hours: String(formData.get("hours") ?? "").trim() || null,
status: String(formData.get("status") ?? "pending") as "active" | "pending",
highlighted: formData.get("highlighted") === "on",
})
.where(eq(members.id, id));
-5
View File
@@ -152,11 +152,6 @@ export default async function EditMemberPage({
<textarea name="hours" rows={4} defaultValue={member.hours ?? ""} className="field" style={{ resize: "vertical" }} placeholder={"Mardi – Vendredi|7h – 19h30\nSamedi|7h – 19h\nDimanche|7h – 13h\nLundi|Fermé"} />
</div>
<label style={{ display: "flex", alignItems: "center", gap: 9, marginTop: 16, fontSize: 14, color: "#3c3322", cursor: "pointer" }}>
<input type="checkbox" name="highlighted" defaultChecked={member.highlighted} />
Mettre à l&apos;honneur sur la page d&apos;accueil
</label>
<div style={{ display: "flex", gap: 10, marginTop: 22 }}>
<button
type="submit"
+35
View File
@@ -351,3 +351,38 @@ button {
color: var(--muted);
margin-bottom: 6px;
}
/* ---- Carrousel d'adhérents (défilement automatique) ---- */
@keyframes pleinr-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.pleinr-marquee {
overflow: hidden;
position: relative;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.pleinr-marquee-track {
display: flex;
gap: 18px;
width: max-content;
animation-name: pleinr-marquee;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.pleinr-marquee:hover .pleinr-marquee-track {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.pleinr-marquee-track {
animation: none;
}
.pleinr-marquee {
overflow-x: auto;
}
}
+8 -62
View File
@@ -4,14 +4,13 @@ import { Reveal } from "@/components/Reveal";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import { MembershipModalButton } from "@/components/MembershipModalButton";
import { getHighlightedMembers, getLivePromotions } from "@/lib/queries";
import { MembersCarousel } from "@/components/MembersCarousel";
import { getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
export const dynamic = "force-dynamic";
const STRIPE_WARM =
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
const STRIPE_COOL =
"repeating-linear-gradient(45deg,#eef0ec,#eef0ec 12px,#e2e8e6 12px,#e2e8e6 24px)";
function badgeColor(badge: string | null) {
if (!badge) return "#1f8a5b";
@@ -19,9 +18,9 @@ function badgeColor(badge: string | null) {
}
export default async function AccueilPage() {
const [promos, highlighted] = await Promise.all([
const [promos, rotatingMembers] = await Promise.all([
getLivePromotions(6),
getHighlightedMembers(3),
getRotatingActiveMembers(),
]);
return (
@@ -39,7 +38,7 @@ export default async function AccueilPage() {
{/* hero */}
<section
className="grid hero-grid"
style={{ position: "relative", padding: "30px 0 50px", overflow: "hidden" }}
style={{ position: "relative", padding: "30px 0 50px", overflow: "hidden", alignItems: "center" }}
>
<Sparkle color="#E0A63C" size={22} style={{ top: 30, left: 20 }} duration={3.2} />
<Sparkle color="#6FB0C6" size={15} style={{ bottom: 64, left: 120 }} duration={2.6} delay={0.4} />
@@ -307,70 +306,17 @@ export default async function AccueilPage() {
</Reveal>
</section>
{/* highlighted members */}
{/* à l'honneur — carrousel auto */}
<section style={{ padding: "40px 0 44px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
<h2 className="font-display" style={{ fontWeight: 800, fontSize: 26, margin: 0, color: "#26201a" }}>
À l&apos;honneur ce mois-ci
À l&apos;honneur
</h2>
<a href="/annuaire" className="link-arrow" style={{ textDecoration: "none", color: "#9a6638", fontWeight: 700, fontSize: 14.5 }}>
Tout l&apos;annuaire →
</a>
</div>
<Reveal className="grid grid-3 reveal-stagger" style={{ gap: 18 }}>
{highlighted.map((m, i) => (
<Link
key={m.id}
href={`/adherents/${m.id}`}
className="lift-card"
style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 18, overflow: "hidden", textDecoration: "none", color: "inherit", display: "block" }}
>
<div
style={{
position: "relative",
height: 150,
background: i % 2 === 1 ? STRIPE_COOL : STRIPE_WARM,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
photo vitrine
</span>
{m.categoryLabel && (
<span
style={{
position: "absolute",
top: 12,
left: 12,
background: "#9a6638",
color: "#fff",
borderRadius: 999,
padding: "5px 12px",
fontSize: 11,
fontWeight: 700,
}}
>
{m.categoryLabel}
</span>
)}
</div>
<div style={{ padding: "16px 18px 18px" }}>
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 18, margin: "0 0 5px", color: "#26201a" }}>
{m.name}
</h3>
<p style={{ margin: "0 0 12px", fontSize: 13.5, color: "#8c8068", lineHeight: 1.5 }}>
{m.description}
</p>
<div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, color: "#6c6150" }}>
<span style={{ width: 7, height: 7, borderRadius: "50%", background: m.accent ?? "#E0A63C" }} />
{[m.address, m.city].filter(Boolean).join(" · ")}
</div>
</div>
</Link>
))}
</Reveal>
<MembersCarousel members={rotatingMembers} />
</section>
{/* warm CTA */}
+92
View File
@@ -0,0 +1,92 @@
"use client";
import Link from "next/link";
export type CarouselMember = {
id: number;
name: string;
description: string | null;
city: string | null;
address: string | null;
coverUrl: string | null;
logoUrl: string | null;
categoryLabel: string | null;
accent: string | null;
};
const STRIPE_WARM =
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
function Card({ m }: { m: CarouselMember }) {
const image = m.coverUrl || m.logoUrl;
return (
<Link
href={`/adherents/${m.id}`}
className="lift-card"
style={{
flex: "0 0 300px",
width: 300,
background: "#fff",
border: "1px solid #e6dcc6",
borderRadius: 18,
overflow: "hidden",
textDecoration: "none",
color: "inherit",
display: "block",
}}
>
<div
style={{
position: "relative",
height: 150,
background: image ? `center/cover no-repeat url(${image})` : STRIPE_WARM,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{!image && (
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
photo vitrine
</span>
)}
{m.categoryLabel && (
<span style={{ position: "absolute", top: 12, left: 12, background: "#9a6638", color: "#fff", borderRadius: 999, padding: "5px 12px", fontSize: 11, fontWeight: 700 }}>
{m.categoryLabel}
</span>
)}
</div>
<div style={{ padding: "16px 18px 18px" }}>
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 18, margin: "0 0 5px", color: "#26201a" }}>
{m.name}
</h3>
<p style={{ margin: "0 0 12px", fontSize: 13.5, color: "#8c8068", lineHeight: 1.5, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>
{m.description}
</p>
<div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, color: "#6c6150" }}>
<span style={{ width: 7, height: 7, borderRadius: "50%", background: m.accent ?? "#E0A63C" }} />
{[m.address, m.city].filter(Boolean).join(" · ")}
</div>
</div>
</Link>
);
}
export function MembersCarousel({ members }: { members: CarouselMember[] }) {
if (members.length === 0) return null;
// Vitesse proportionnelle au nombre de cartes (≈ 6 s par carte).
const duration = Math.max(20, members.length * 6);
// Doublé pour un défilement en boucle sans couture.
const loop = [...members, ...members];
return (
<div className="pleinr-marquee" aria-label="Adhérents à l'honneur">
<div className="pleinr-marquee-track" style={{ animationDuration: `${duration}s` }}>
{loop.map((m, i) => (
<Card key={`${m.id}-${i}`} m={m} />
))}
</div>
</div>
);
}
+26
View File
@@ -122,6 +122,32 @@ export async function getMemberLivePromotions(memberId: number) {
.orderBy(desc(promotions.createdAt));
}
// Tous les adhérents actifs, tournés équitablement : l'ordre pivote d'un cran
// par heure pour que chacun passe en tête à tour de rôle au fil du temps.
export async function getRotatingActiveMembers() {
const rows = await db
.select({
id: members.id,
name: members.name,
description: members.description,
city: members.city,
address: members.address,
coverUrl: members.coverUrl,
logoUrl: members.logoUrl,
categoryLabel: categories.label,
accent: categories.accent,
})
.from(members)
.leftJoin(categories, eq(members.categoryId, categories.id))
.where(eq(members.status, "active"))
.orderBy(asc(members.id));
const n = rows.length;
if (n <= 1) return rows;
const offset = Math.floor(Date.now() / 3_600_000) % n;
return [...rows.slice(offset), ...rows.slice(0, offset)];
}
export async function getHighlightedMembers(limit = 3) {
const rows = await db
.select({