mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-15 03:09:55 +02:00
L'annuaire, les pages métier et la fiche adhérent montrent désormais la personne à joindre chez l'adhérent — nom, prénom, ligne directe — mais uniquement à un visiteur connecté. Trois colonnes sur `members` (`contact_first_name`, `contact_last_name`, `contact_phone`), saisies dans les deux formulaires de fiche via le composant partagé `MemberContactFields`. Le filtrage ne repose pas sur du CSS ni sur un rendu conditionnel tardif : aucune requête publique ne lit ces colonnes. Seules `getMemberContacts()` et `getMemberContact()` les rapatrient, et les pages ne les appellent qu'après un `getSession()` positif. Un visiteur anonyme ne reçoit donc rien — ni dans le HTML, ni dans le JSON-LD, ni dans les props du composant client de l'annuaire. `tests/security.test.ts` verrouille cette séparation. `src/lib/member-contact.ts` reste pur (composition du nom, lien `tel:`) et est couvert par `tests/member-contact.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012TAK3c4jAUqxVMzy746wWQ
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
/**
|
|
* Référent d'un adhérent : la personne à joindre, sa ligne directe.
|
|
*
|
|
* Ces informations sont **réservées aux visiteurs connectés** : elles ne
|
|
* doivent jamais partir dans le HTML public, le JSON-LD ou les métadonnées.
|
|
* Le module est pur — le filtrage par session se fait chez l'appelant, qui
|
|
* n'appelle `memberContact()` qu'après avoir vérifié la session, et verrouillé
|
|
* par `tests/member-contact.test.ts`.
|
|
*/
|
|
|
|
export type MemberContactSource = {
|
|
contactFirstName?: string | null;
|
|
contactLastName?: string | null;
|
|
contactPhone?: string | null;
|
|
};
|
|
|
|
export type MemberContact = {
|
|
/** « Prénom Nom », ou l'un des deux seulement. */
|
|
name: string | null;
|
|
phone: string | null;
|
|
};
|
|
|
|
function clean(value: string | null | undefined) {
|
|
const trimmed = (value ?? "").replace(/\s+/g, " ").trim();
|
|
return trimmed || null;
|
|
}
|
|
|
|
/** « Prénom Nom » à partir des deux champs, `null` si les deux sont vides. */
|
|
export function contactName(source: MemberContactSource): string | null {
|
|
return clean([clean(source.contactFirstName), clean(source.contactLastName)].filter(Boolean).join(" "));
|
|
}
|
|
|
|
/**
|
|
* Le référent affichable, ou `null` s'il n'y a rien à montrer : le bloc
|
|
* « Contact adhérent » disparaît alors plutôt que d'afficher un cadre vide.
|
|
*/
|
|
export function memberContact(source: MemberContactSource): MemberContact | null {
|
|
const name = contactName(source);
|
|
const phone = clean(source.contactPhone);
|
|
if (!name && !phone) return null;
|
|
return { name, phone };
|
|
}
|
|
|
|
/**
|
|
* `tel:` à partir d'un numéro saisi librement (« 03 83 24 .. », « +33 3 83 … »).
|
|
* Seuls les chiffres sont conservés, avec l'indicatif international s'il ouvre
|
|
* le numéro ; une saisie sans aucun chiffre ne produit pas de lien.
|
|
*/
|
|
export function telHref(phone: string | null | undefined): string | null {
|
|
const raw = clean(phone);
|
|
if (!raw) return null;
|
|
const international = raw.startsWith("+");
|
|
const digits = raw.replace(/\D/g, "");
|
|
if (!digits) return null;
|
|
return `tel:${international ? "+" : ""}${digits}`;
|
|
}
|