Refonte du bloc infos pratiques, suspension des promos et publication réseaux

Bloc « Informations pratiques » (fiche adhérent)
- Suppression des dégradés hors palette (radial doré de la section, dégradés
  crème/bleu des deux sous-cartes) au profit du vocabulaire visuel du site :
  carte blanche bordée, sous-cartes plates sur le fond de champ, variables CSS.
- La pastille « Préparer votre visite » devient une puce arrondie identique aux
  badges du reste du site ; le chip d'ouverture reprend les teintes de statut.
- Les glyphes texte ◷ et ↗ sont remplacés par des icônes SVG cohérentes.

Suspension des promotions
- Nouveau statut `suspended` + colonnes `suspended_by` / `suspended_by_id` /
  `suspended_at`.
- L'adhérent suspend et réactive ses propres promotions depuis Mon espace ;
  l'association suspend et réactive n'importe laquelle depuis le backoffice.
- Une suspension décidée par l'association ne peut pas être levée par
  l'adhérent : le motif est affiché dans son espace.
- Les lectures publiques filtrant déjà sur `live`, une promo suspendue quitte
  immédiatement l'accueil, l'annuaire et la fiche.

Publication Facebook / LinkedIn
- `src/lib/social.ts` : publication d'une promo en ligne sur la page Facebook
  (Graph API, upload multipart car les images sont en data-URI) et sur la page
  LinkedIn (Posts API + Images API en trois étapes).
- Jetons d'accès exclusivement en variables d'environnement ; un réseau non
  configuré masque son bouton.
- Table `social_posts` : chaque tentative est tracée, un échec est rattrapé et
  affiché sur la carte de la promo sans interrompre le backoffice.
- Nouvelle capacité RBAC `publishSocial` (admin + modérateur).

Liens sociaux publics
- Réglages `association_facebook` / `association_linkedin` éditables dans
  Paramètres, préremplis avec les pages de l'association.
- Section « Plein R sur les réseaux » sur l'accueil et icônes dans le pied de
  page, masquées si le réglage est vide.

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-07-24 22:20:51 +00:00
1 parent dd9697019c
commit e3a4771edf
20 files changed
+2370 -109

No files matched your search

+101 -1
View File
@@ -7,7 +7,9 @@ import { MembershipModalButton } from "@/components/MembershipModalButton";
import { MembersCarousel } from "@/components/MembersCarousel";
import { StarField } from "@/components/StarField";
import { PromoImage } from "@/components/PromoImage";
import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons";
import { getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
import { getSiteSettings, socialLinks } from "@/lib/site-settings";
export const dynamic = "force-dynamic";
@@ -20,10 +22,12 @@ function badgeColor(badge: string | null) {
}
export default async function AccueilPage() {
const [promos, rotatingMembers] = await Promise.all([
const [promos, rotatingMembers, settings] = await Promise.all([
getLivePromotions(6),
getRotatingActiveMembers(),
getSiteSettings(),
]);
const socials = socialLinks(settings);
return (
<div
@@ -318,6 +322,102 @@ export default async function AccueilPage() {
<MembersCarousel members={rotatingMembers} />
</section>
{/* réseaux sociaux */}
{socials.length > 0 && (
<section
style={{
position: "relative",
margin: "0 0 46px",
background: "#fff",
border: "1px solid #e6dcc6",
borderRadius: 24,
padding: "34px 36px 36px",
overflow: "hidden",
boxShadow: "0 26px 60px -40px rgba(40,30,15,0.5)",
}}
>
<Sparkle color="#6FB0C6" size={18} style={{ top: 26, right: 42 }} duration={3.1} />
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: 8,
background: "#eaf0f6",
color: "#2C6FB3",
fontSize: 12,
letterSpacing: "0.12em",
textTransform: "uppercase",
fontWeight: 800,
padding: "6px 13px",
borderRadius: 999,
marginBottom: 12,
}}
>
<span style={{ width: 8, height: 8, borderRadius: "50%", background: "#2C6FB3" }} />
Suivez-nous
</div>
<h2
className="font-display"
style={{ fontWeight: 800, fontSize: "clamp(22px, 5vw, 30px)", letterSpacing: "-0.02em", margin: 0, color: "#26201a" }}
>
Plein R sur les réseaux
</h2>
<p style={{ margin: "7px 0 0", color: "#8c8068", fontSize: 15, maxWidth: 560 }}>
Bons plans, rencontres et actualités des commerçants du Bassin de Pompey : retrouvez-nous
sur Facebook et LinkedIn.
</p>
<div className="grid grid-2" style={{ gap: 16, marginTop: 24 }}>
{socials.map((s) => (
<a
key={s.network}
href={s.href}
target="_blank"
rel="noopener noreferrer"
className="lift-card"
style={{
display: "flex",
alignItems: "center",
gap: 14,
textDecoration: "none",
color: "inherit",
background: "#faf7ef",
border: "1px solid #f0e8d6",
borderRadius: 16,
padding: "16px 18px",
}}
>
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 44,
height: 44,
borderRadius: 13,
background: SOCIAL_BRAND[s.network],
color: "#fff",
flexShrink: 0,
}}
>
<SocialIcon network={s.network} size={22} />
</span>
<span style={{ minWidth: 0, flex: 1 }}>
<span className="font-display" style={{ display: "block", fontWeight: 700, fontSize: 16, color: "#26201a" }}>
{s.label}
</span>
<span style={{ display: "block", fontSize: 12.5, color: "#8c8068", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{s.handle}
</span>
</span>
<span style={{ color: "#2C6FB3", fontWeight: 700, fontSize: 14, flexShrink: 0 }}>↗</span>
</a>
))}
</div>
</section>
)}
{/* warm CTA */}
<section
style={{