L'application publie le fichier tel quel sur Facebook et LinkedIn : aucun
recadrage, aucune marge. Un visuel qui n'est pas au format attendu est donc
recadré — ou entouré de bandes de couleur prises dans l'image — par la
plateforme, ce qui ne se voit nulle part côté site puisque les cartes
recadrent en `cover`.
Le formulaire de dépôt affiche maintenant les dimensions, le format et le
poids du fichier choisi, et dit ce que les réseaux en feront : format non
carré, image trop petite pour un fil d'actualité, fichier trop lourd pour
l'envoi. Purement informatif, rien n'est bloqué.
`src/lib/image-info.ts` est pur et verrouillé par `tests/image-info.test.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS61rkgTUGgPYH88tuVx9U
La colonne de droite portait un aperçu étiré sur toute sa largeur : le visuel
carré rendait la carte démesurée, très loin de ce que verra le visiteur.
- L'aperçu est borné à 320 px, la largeur d'une carte dans la grille du site.
- Au-dessus, un récapitulatif « Dates de votre offre » : durée de validité et
moment de publication, les deux informations que la carte ne montre pas.
Sans saisie il affiche les valeurs par défaut en gris (« Sans date limite »,
« Dès la validation par l'association ») ; des bornes incohérentes s'y
signalent en rouge, comme sous le champ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
L'angle de la carte est rendu à la catégorie ; la pastille du commerce se
place devant son nom, en pied de carte, là où on lit déjà qui propose l'offre.
La période de validité passe sous le nom, dans le même bloc.
Appliqué à la carte d'accueil, à l'aperçu en direct de l'espace adhérent et à
la carte de modération. Retiré de la carte de la fiche adhérent : elle n'y
affiche pas de nom de commerce, et le logo figure déjà en tête de page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Le logo du commerce identifie l'offre avant même qu'on lise le nom. Il est
posé en pastille ronde dans l'angle haut-gauche de la carte, la catégorie se
décalant juste à côté sur la même ligne.
- `src/components/MemberAvatar.tsx` : logo en `contain` sur fond blanc (les
logos d'adhérents sont des marques sur fond clair, un `cover` les rognerait),
repli sur les initiales colorées quand la fiche n'a pas de logo.
- Appliqué à la carte d'accueil, à la carte de la fiche adhérent, à l'aperçu
en direct de l'espace adhérent et à la carte de modération : même composition
partout, l'aperçu montre donc bien ce que verra le visiteur.
- `getLivePromotions` remonte `members.logo_url` ; la page espace le passe au
formulaire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Deux comportements demandés :
1. Une promotion n'apparaît sur le site que pendant sa période de validité.
Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) :
statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en
Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux
heures trop tôt. Le statut n'est pas touché : hors période, le backoffice
explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle
revient d'elle-même si les dates changent.
2. La publication peut être programmée. `promotions.publish_at` : l'adhérent
propose une date, le modérateur la garde, la déplace ou la vide dans le
formulaire « Valider ». Avec une échéance future la promotion passe au
statut `scheduled` — rien n'est visible, rien n'est diffusé — et
`releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux
ensemble. Un bouton « Publier maintenant » court-circuite l'attente.
Mécanique :
- `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄
heure de l'association, changements d'heure compris, et refus d'une saisie
illisible plutôt qu'une publication immédiate involontaire.
- `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants
désormais, une action serveur et la boucle de libération, qui n'a pas de
requête et ne peut donc pas appeler `revalidatePath()`. Idem pour
`logActivity()` dans `src/lib/activity-log.ts`.
- La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le
statut : atomique, donc pas de double publication même à plusieurs
instances, en plus de la garde existante sur `social_posts`.
- Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage
au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans
`instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` :
Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut
pas être embarqué.
Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Une promotion peut désormais porter une date de début et une date de fin,
toutes deux facultatives et indépendantes. Sans date, rien n'est affiché.
- Schéma : `promotions.starts_on` / `ends_on` (`date`), migration 0015.
- `src/lib/promo-validity.ts` (pur, testé) : `formatValidity()` produit
« Valable du 1er au 15 mars 2027 », « Valable jusqu'au … », « Valable à
partir du … » en repliant mois et année quand ils se répètent ;
`formatValidityShort()` pour les lignes de méta ; `isRangeInvalid()` pour
la validation.
- Espace adhérent : deux champs date, aperçu en direct de la phrase et
blocage de l'envoi si la fin précède le début.
- `publishPromo` relit et revalide les bornes côté serveur.
- Affichage : carte d'accueil, fiche adhérent, liste de l'adhérent, carte de
modération — et le message Facebook / LinkedIn, via le même formateur.
- L'ancien texte libre `valid_until` reste comme repli d'affichage pour les
promotions de démonstration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
- Espace adhérent : /backend/espace (profil, inscriptions, droit à l'image)
et /backend/espace/promotions (dépôt et suivi). Bandeau et onglets communs
(EspaceHeader) avec le nombre d'offres en ligne ; la page promotions ouvre
sur « Mes promotions en cours », puis le formulaire, puis les offres en
attente et l'historique. Deux entrées dans le menu, titres de page dédiés,
revalidation des deux chemins.
- Fiche adhérent : colonne members.contact_email (migration 0013), saisie
dans l'espace adhérent et la fiche staff ; la fiche publique et le JSON-LD
affichent contact_email, sinon l'e-mail du compte.
- VitrineImage : sans photo de couverture, le logo est contenu à ~60 % d'un
cadre à hauteur fixe sur un fond du logo flouté ; le conteneur sans hauteur
qui laissait le logo déborder est supprimé.
- Promotions : la catégorie devient un type de produit ou de service
(src/lib/promo-categories.ts, 10 groupes, ~85 entrées) pré-sélectionné
depuis le métier de l'adhérent, à la place de la liste des métiers.
- Tests : catégories de promo (largeur, unicité, pré-sélection pour chaque
métier du référentiel).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
Le bouton libre « Publier sur X » permettait de diffuser n'importe quelle promo
en ligne, à tout moment. Le choix appartient désormais à l'adhérent, la
publication part à la validation, et le choix est figé ensuite.
- Colonnes `promotions.share_facebook` / `share_linkedin` : réseaux demandés à la
soumission, modifiables tant que la promo est `pending`.
- L'adhérent coche les réseaux dans le formulaire de soumission et peut encore
les changer depuis Mes promotions tant qu'elle est en attente ; une fois
validée, il ne voit plus qu'un état en lecture seule.
- Le modérateur retrouve ces cases pré-cochées dans le formulaire « Valider » et
peut les ajuster : c'est le dernier moment où la diffusion est modifiable.
Valider met en ligne puis publie, dans cet ordre, afin qu'un réseau en panne
ne bloque pas la mise en ligne.
- `publishPromoShares` devient le seul point de publication. Elle ne lève jamais
et ignore tout réseau ayant déjà une publication réussie : une promo n'est
jamais postée deux fois, y compris après un cycle suspension → remise en ligne.
- `sharePromoToSocial` est remplacée par `retryPromoShare`, limitée au rattrapage
d'un échec sur un réseau déjà choisi, sur une promo en ligne uniquement.
- Un réseau non configuré sur le serveur enregistre un échec explicite plutôt
que d'échouer en silence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
Les visuels promo sont carrés (posts Facebook) : un cadre 1200/630 laissait des
bandes floues sur les côtés. Passage du cadre image en 1:1 partout (accueil,
fiche, aperçu espace adhérent, modération) → les images carrées remplissent
sans bords. Conseil d'upload : image carrée (1080×1080).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
Aligne les cartes promo sur la dernière refonte Claude Design : la zone image
passe d'une hauteur fixe à un ratio 1200/630, au-dessus du composant PromoImage
existant (remplissage flou + contain). Appliqué à l'accueil, la fiche adhérent,
l'aperçu de l'espace adhérent et la modération. Conseil d'upload : 1200×630.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
Replace cover-crop background with PromoImage component: sharp
contain image over a blurred cover copy. Whole image visible on
any aspect ratio, no flat letterbox bars. Applied to accueil,
adherent profile, backend promotions, member space preview +
upload dropzone.
Also retheme adherent logo frame (cream gradient + warm border)
instead of flat white.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed enrichi avec ~59 catégories (bouche, mode/beauté, maison/bâtiment, auto,
santé, services aux particuliers & entreprises, loisirs/culture, industrie),
slugs existants conservés. Le select « Catégorie » de l'espace adhérent lit
désormais la liste depuis la base (plus de liste codée en dur). Insertion
idempotente : relancer le seed ajoute les nouvelles catégories sans doublon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL