Espace adhérent en deux pages, e-mail de contact, logo sans débordement, types de produits pour les promos

- 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
This commit is contained in:
Claude committed 2026-09-06 09:02:27 +00:00
1 parent 8b46cd6316
commit 23d1d14a49
18 files changed
+2289 -278

No files matched your search

+19 -7
View File
@@ -6,6 +6,7 @@ import { publishPromo } from "../actions";
import { PromoImage } from "@/components/PromoImage";
import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons";
import type { SocialNetwork } from "@/lib/social";
import type { PromoCategoryGroup } from "@/lib/promo-categories";
const STRIPE_WARM =
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
@@ -17,18 +18,23 @@ const NETWORK_LABELS: Record<SocialNetwork, string> = {
export function MemberSpaceForm({
memberName,
categories,
categoryGroups,
defaultCategory,
networks = [],
}: {
memberName: string;
categories: string[];
/** Types de produits / services, groupés pour le `<select>`. */
categoryGroups: PromoCategoryGroup[];
/** Pré-sélection déduite du métier de l'adhérent. */
defaultCategory?: string;
networks?: SocialNetwork[];
}) {
const CATEGORIES = categories.length > 0 ? categories : ["Autre"];
const allCategories = categoryGroups.flatMap((g) => g.items);
const initialCategory = defaultCategory && allCategories.includes(defaultCategory) ? defaultCategory : allCategories[0] ?? "Autre";
const router = useRouter();
const [title, setTitle] = useState("");
const [text, setText] = useState("");
const [cat, setCat] = useState(CATEGORIES[0]);
const [cat, setCat] = useState(initialCategory);
const [badge, setBadge] = useState("");
const [imgData, setImgData] = useState("");
const [shares, setShares] = useState<SocialNetwork[]>([]);
@@ -144,10 +150,16 @@ export function MemberSpaceForm({
<div style={{ display: "flex", gap: 12, marginBottom: 16 }}>
<div style={{ flex: 1 }}>
<label className="field-label">Catégorie</label>
<label className="field-label">Type de produit ou de service</label>
<select value={cat} onChange={(e) => setCat(e.target.value)} className="field">
{CATEGORIES.map((c) => (
<option key={c}>{c}</option>
{categoryGroups.map((group) => (
<optgroup key={group.label} label={group.label}>
{group.items.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</optgroup>
))}
</select>
</div>