// @mostajs/qrpanel/client — React component // Author: Dr Hamid MADANI // // Composant `` agnostique de l'app. Affiche un QR + l'URL // textuelle copiable + 3 actions (copier, ouvrir, mailto). // Le composant ne génère pas le QR lui-même : l'app fournit le `qrSrc` // (URL d'un endpoint API ou data URL inline). // // Style : Tailwind classes — l'app les compile dans son build (le // component reste portable car les classes sont des chaînes). // // Note: marqué 'use client' pour Next.js App Router. Pour SPA (Vite, // CRA, etc.), la directive est ignorée — le component fonctionne tel // quel. 'use client' import { useState, useId } from 'react' export interface QrPanelMode { /** Identifiant interne du mode (libre — sera passé en argument à `qrSrc`). */ key: string /** Libellé bouton. */ label: string /** URL absolue à afficher / copier dans ce mode. */ url: string /** URL de l'image QR pour ce mode (endpoint API ou data URL). */ qrSrc: string /** Description courte affichée sous l'en-tête. */ description?: string } export interface QrPanelProps { /** Liste des modes proposés (1 ou plusieurs). */ modes: QrPanelMode[] /** Mode initial — index dans `modes`. Default 0. */ initialModeIndex?: number /** Titre du panel. Default 'QR code & lien d\'invitation'. */ title?: string /** Pré-remplissage `mailto`. Default 'Invitation'. */ mailSubject?: string /** Pré-remplissage `mailto` body — `{url}` est remplacé par l'URL courante. */ mailBodyTemplate?: string /** * Destinataires placés dans `To:` du `mailto:` *(visibles entre eux)*. * Liste d'emails (array ou string CSV). Combinable avec `mailBcc`. */ mailTo?: string[] | string /** * Destinataires placés dans `Bcc:` *(invisibles entre eux — recommandé * pour les listes de cohort où les participants ne doivent pas voir * les adresses des autres)*. Liste d'emails (array ou string CSV). */ mailBcc?: string[] | string /** Taille de l'image QR. Default 260. */ qrSize?: number /** Classe CSS racine optionnelle. */ className?: string } const DEFAULT_BODY = 'Bonjour,\n\nVoici ton lien personnel :\n\n{url}\n\n' /** Accepte une string CSV ou un array, normalise en array d'emails non-vides. */ function normaliseRecipients(input: string[] | string | undefined): string[] { if (!input) return [] const arr = Array.isArray(input) ? input : input.split(',') return arr.map(s => s.trim()).filter(s => s.length > 0) } export function QrPanel({ modes, initialModeIndex = 0, title = 'QR code & lien d\'invitation', mailSubject = 'Invitation', mailBodyTemplate = DEFAULT_BODY, mailTo, mailBcc, qrSize = 260, className = '', }: QrPanelProps) { const [idx, setIdx] = useState(Math.min(Math.max(0, initialModeIndex), modes.length - 1)) const [copied, setCopied] = useState(false) const textareaId = useId() const current = modes[idx] ?? modes[0] if (!current) return null const toList = normaliseRecipients(mailTo) const bccList = normaliseRecipients(mailBcc) const parts: string[] = [] parts.push(`subject=${encodeURIComponent(mailSubject)}`) parts.push(`body=${encodeURIComponent(mailBodyTemplate.replace('{url}', current.url))}`) if (bccList.length > 0) parts.push(`bcc=${encodeURIComponent(bccList.join(','))}`) // `To:` est positionné juste après `mailto:` quand fourni (les // destinataires To: sont rendus visibles entre eux par tous les clients). // Les Bcc: passent par query param `&bcc=`. const toPart = toList.length > 0 ? encodeURIComponent(toList.join(',')) : '' const mailto = `mailto:${toPart}?${parts.join('&')}` const recipientCount = toList.length + bccList.length async function copy() { try { await navigator.clipboard.writeText(current.url) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { const el = document.getElementById(textareaId) as HTMLTextAreaElement | null el?.select() } } return (

{title}

{modes.length > 1 && (
{modes.map((m, i) => ( ))}
)}
{current.description && (

{current.description}

)}
{`QR ⬇ Télécharger le PNG