/* global React, ReactDOM */ const { useState, useEffect, useRef, useMemo } = React; // ============================================================ // Carte Famili — Certificat d'inscription au régime de // couverture santé familiale — bilingual editor // (Original design — not affiliated with CNSS) // ============================================================ const TWEAKS_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "teal", "pattern": "octagram", "showLogo": true, "showWatermark": true, "layout": "classic" }/*EDITMODE-END*/; const THEMES = { teal: { ink: "oklch(0.36 0.05 200)", brand: "oklch(0.50 0.09 195)", accent: "oklch(0.72 0.10 75)", paper: "oklch(0.985 0.012 85)", soft: "oklch(0.94 0.018 195)" }, plum: { ink: "oklch(0.30 0.05 320)", brand: "oklch(0.46 0.10 330)", accent: "oklch(0.74 0.10 60)", paper: "oklch(0.985 0.010 320)", soft: "oklch(0.94 0.018 320)" }, olive: { ink: "oklch(0.32 0.04 130)", brand: "oklch(0.48 0.08 140)", accent: "oklch(0.72 0.10 80)", paper: "oklch(0.985 0.012 100)", soft: "oklch(0.93 0.020 130)" }, ink: { ink: "oklch(0.20 0.01 250)", brand: "oklch(0.30 0.02 250)", accent: "oklch(0.70 0.13 50)", paper: "oklch(0.985 0.005 250)", soft: "oklch(0.93 0.008 250)" }, rose: { ink: "oklch(0.34 0.06 20)", brand: "oklch(0.50 0.11 20)", accent: "oklch(0.74 0.10 60)", paper: "oklch(0.985 0.012 30)", soft: "oklch(0.94 0.020 20)" }, }; // Relationship options — adapt to spouse type ---------------------- const SPOUSE_OPTIONS = { wife: { relAr: "الزوجة", relFr: "Épouse" }, husband: { relAr: "الزوج", relFr: "Époux" }, none: { relAr: "—", relFr: "—" }, }; const CHILD_OPTIONS = [ { value: "son", relAr: "ابن", relFr: "Fils" }, { value: "daughter", relAr: "ابنة", relFr: "Fille" }, ]; // ---- form state ---------------------------------------------------- const initialData = { cardNumber: "921990126", cin: "C689324", idcs: "1234567890", lastNameLat: "FENNICHA", firstNameLat: "ABDELLAZIZ", lastNameAr: "فنيشة", firstNameAr: "عبد العزيز", birthDate: "1970-02-15", issueDate: "2023-05-23", titulaireGender: "male", footerFr: "Conçu par Brahim Es-Sbai · Écrivain Public", footerAr: "من تصميم إبراهيم الصبائي · كاتب عمومي", // male => spouse is wife; female => husband hasSpouse: true, spouse: { nameAr: "بوبدارة حفيظة", id: "9159436153", birthDate: "1975-08-12", }, children: [ { nameAr: "فنيشة غزلان", id: "5245986805", birthDate: "2000-04-10", kind: "daughter" }, { nameAr: "فنيشة ابراهيم", id: "4510519488", birthDate: "2003-11-22", kind: "son" }, { nameAr: "فنيشة معاد", id: "8299493429", birthDate: "2007-06-03", kind: "son" }, { nameAr: "فنيشة اية", id: "7817893042", birthDate: "2010-09-15", kind: "daughter" }, ], }; // ---- helpers ------------------------------------------------------- function fmtDate(iso) { if (!iso) return "—"; const [y, m, d] = iso.split("-"); if (!y) return iso; return `${d}/${m}/${y}`; } function spaceDigits(s) { return (s || "").split("").join("\u2009"); } function spouseRel(gender) { return gender === "female" ? SPOUSE_OPTIONS.husband : SPOUSE_OPTIONS.wife; } function childRel(kind) { return CHILD_OPTIONS.find(c => c.value === kind) || CHILD_OPTIONS[0]; } // ===================================================================== // Geometric pattern — original octagram tessellation // ===================================================================== function PatternDefs({ id, color, kind }) { if (kind === "rings") { return ( ); } if (kind === "weave") { return ( ); } if (kind === "lines") { return ( ); } return ( ); } function PatternBackground({ kind, color, opacity = 0.18 }) { const id = `pat-${kind}`; return ( ); } // ===================================================================== // Original logo — three nested family shields // ===================================================================== function CFLogo({ color, accent, size = 56 }) { return ( ); } // ===================================================================== // COMPACT CARD — for PNG download at business card size (54×85.6mm) // Rendered at 600×950px (~280 DPI for crisp print) // ===================================================================== function CardCompact({ data, theme }) { const t = THEMES[theme] || THEMES.teal; // Total family members count const totalCount = 1 + (data.hasSpouse ? 1 : 0) + data.children.length; // When totalCount >= 6, the holder + spouse info moves to the back card. // The recto then shows ONLY the children (or nothing if no children). const moveHolderToBack = totalCount >= 6; // The list shown on the recto under "Bénéficiaires" const beneficiaries = useMemo(() => { const list = []; // For 1 or 2 total: show holder (and spouse) — uses single/pair layout // For 3+ total: omit holder from the list (no duplication) // For 6+ total: also omit spouse (moved to back), show only children if (totalCount <= 2) { list.push({ nameAr: `${data.lastNameAr} ${data.firstNameAr}`.trim(), id: data.idcs, birthDate: data.birthDate, relAr: "صاحب(ة) البطاقة", }); if (data.hasSpouse) { const r = spouseRel(data.titulaireGender); list.push({ nameAr: data.spouse.nameAr, id: data.spouse.id, birthDate: data.spouse.birthDate, relAr: r.relAr, }); } } else if (!moveHolderToBack && data.hasSpouse) { // 3-5 total: show spouse but skip holder const r = spouseRel(data.titulaireGender); list.push({ nameAr: data.spouse.nameAr, id: data.spouse.id, birthDate: data.spouse.birthDate, relAr: r.relAr, }); } // For 6+ total, holder & spouse are on the back — list only children below data.children.forEach(c => { const r = childRel(c.kind); list.push({ nameAr: c.nameAr, id: c.id, birthDate: c.birthDate, relAr: r.relAr }); }); return list; }, [data, totalCount, moveHolderToBack]); return (
{/* Top accent bar */}
{/* Header — CNSS logo (left) | AMO TADAMON (center) | Morocco logo (right) */}
CNSS
AMO TADAMON
Régime d'assurance maladie obligatoire
Royaume du Maroc
{/* Card number */}
N° d'immatriculation · رقم التسجيل
{spaceDigits(data.cardNumber || "—")}
{/* Titulaire identity — bilingual condensed */}
{data.lastNameLat} {data.firstNameLat} {data.lastNameAr} {data.firstNameAr}
CIN: {data.cin || "—"}
IDCS: {data.idcs || "—"}
Né(e): {fmtDate(data.birthDate)}
Inscrit: {fmtDate(data.issueDate)}
{/* Beneficiaries — layout adapts to count */}
Bénéficiaires · {totalCount} المستفيدون
{beneficiaries.length === 1 ? ( /* SINGLE BENEFICIARY — hero card layout */
{beneficiaries[0].relAr}
{beneficiaries[0].nameAr || "—"}
IDCS {beneficiaries[0].id || "—"}
Date de naissance {fmtDate(beneficiaries[0].birthDate)}
) : beneficiaries.length === 2 ? ( /* TWO BENEFICIARIES — paired card layout */
{beneficiaries.map((b, i) => (
{b.relAr}
{b.nameAr || "—"}
IDCS {b.id || "—"}
Né(e) {fmtDate(b.birthDate)}
))}
) : ( /* 3-8 BENEFICIARIES — table layout (no relation column) */
{beneficiaries.map((b, i) => (
{i + 1}
{b.nameAr || "—"}
{b.id || "—"}
{fmtDate(b.birthDate)}
))}
)}
{/* Footer — designer signature (editable) */}
{data.footerFr && {data.footerFr}} {data.footerAr && {data.footerAr}}
); } // ===================================================================== // CARD BACK — Verso of the business card (54×85.6mm vertical) // Decorative side with logos and brand name only // ===================================================================== function CardBack({ theme, data }) { const t = THEMES[theme] || THEMES.teal; const totalCount = data ? (1 + (data.hasSpouse ? 1 : 0) + data.children.length) : 0; const showHolderInfo = data && totalCount >= 6; return (
{/* Top accent bar */}
{/* Decorative geometric pattern background */} {/* Decorative corner ornaments */} {/* Content */}
{/* Morocco coat of arms — top */}
Royaume du Maroc
المملكة المغربية
Royaume du Maroc
{/* Center divider with ornament */}
{showHolderInfo ? ( /* When 6+ beneficiaries: holder + spouse info on the back */
{/* Card holder */}
صاحب(ة) البطاقة
{`${data.lastNameAr} ${data.firstNameAr}`.trim() || "—"}
{`${data.lastNameLat} ${data.firstNameLat}`.trim()}
IDCS {data.idcs || "—"}
Né(e) {fmtDate(data.birthDate)}
{/* Spouse — only if hasSpouse */} {data.hasSpouse && (
{spouseRel(data.titulaireGender).relAr}
{data.spouse.nameAr || "—"}
IDCS {data.spouse.id || "—"}
Né(e) {fmtDate(data.spouse.birthDate)}
)}
) : ( /* Default decorative back (no holder info) */
AMO TADAMON
آمو تضامن
Régime d'Assurance Maladie Obligatoire
نظام التأمين الإجباري الأساسي عن المرض
)} {/* Center divider */}
{/* CNSS — bottom */}
CNSS
الصندوق الوطني للضمان الاجتماعي
Caisse Nationale de Sécurité Sociale
{/* Bottom accent bar */}
); } // ===================================================================== // CARD // ===================================================================== function Card({ data, theme, pattern, showLogo, showWatermark, layout }) { const t = THEMES[theme] || THEMES.teal; // Build beneficiaries list dynamically const beneficiaries = useMemo(() => { const list = [{ nameAr: `${data.lastNameAr} ${data.firstNameAr}`.trim(), id: data.idcs, birthDate: data.birthDate, relAr: "صاحب(ة) البطاقة", relFr: "Titulaire", }]; if (data.hasSpouse) { const r = spouseRel(data.titulaireGender); list.push({ nameAr: data.spouse.nameAr, id: data.spouse.id, birthDate: data.spouse.birthDate, relAr: r.relAr, relFr: r.relFr, }); } data.children.forEach(c => { const r = childRel(c.kind); list.push({ nameAr: c.nameAr, id: c.id, birthDate: c.birthDate, relAr: r.relAr, relFr: r.relFr }); }); return list; }, [data]); return (
{showWatermark && (
)}
{/* Header */}
{showLogo && (
CNSS
CNSS
)}
AMO TADAMON
Régime d'assurance maladie obligatoire
Certificat d'inscription au régime AMO
pour les personnes incapables de s'acquitter des cotisations
شهادة التسجيل
بنظام التأمين الإجباري الأساسي عن المرض
الخاص بالأشخاص غير القادرين على تحمل واجبات الاشتراك
{/* Number band */}
N° d'immatriculation
{spaceDigits(data.cardNumber || "—")}
رقم التسجيل
{/* Titulaire identity — Arabic on RIGHT, French on LEFT */}
{/* Beneficiaries section — improved table-style */}
Bénéficiaires · {beneficiaries.length} المستفيدون · {beneficiaries.length}
#
الصلة
الاسم الكامل
تاريخ الازدياد
IDCS · المعرف الرقمي
{beneficiaries.map((b, i) => (
{i + 1}
{b.relAr}
{b.nameAr || "—"}
{fmtDate(b.birthDate)}
{b.id || "—"}
))}
{/* Footer removed per user request */}
); } function Field({ label, value }) { return (
{value || "—"}
{label}
); } function FieldAr({ label, value }) { return (
{label}
{value || "—"}
); } // ===================================================================== // FORM // ===================================================================== function Form({ data, setData }) { const update = (k, v) => setData({ ...data, [k]: v }); const updateSpouse = (k, v) => setData({ ...data, spouse: { ...data.spouse, [k]: v } }); const updateChild = (i, k, v) => { const children = data.children.map((c, idx) => idx === i ? { ...c, [k]: v } : c); setData({ ...data, children }); }; const addChild = () => { if (data.children.length >= 8) return; setData({ ...data, children: [...data.children, { nameAr: "", id: "", birthDate: "", kind: "son" }] }); }; const rmChild = (i) => setData({ ...data, children: data.children.filter((_, idx) => idx !== i) }); const spouseLabel = data.titulaireGender === "female" ? "Époux / الزوج" : "Épouse / الزوجة"; return (
update("cardNumber", v)} /> update("cin", v)} /> update("idcs", v)} /> update("lastNameLat", v.toUpperCase())} /> update("firstNameLat", v.toUpperCase())} /> update("lastNameAr", v)} rtl /> update("firstNameAr", v)} rtl /> update("birthDate", v)} /> update("issueDate", v)} /> update("titulaireGender", v)} options={[ { value: "male", label: "Homme · ذكر" }, { value: "female", label: "Femme · أنثى" }, ]} /> update("hasSpouse", v)} /> } > {data.hasSpouse ? ( <> updateSpouse("nameAr", v)} rtl /> updateSpouse("id", v)} /> updateSpouse("birthDate", v)} /> ) : (
Aucun conjoint inclus.
)}
= 8}> + Ajouter } > {data.children.length === 0 && (
Aucun enfant ajouté.
)}
{data.children.map((c, i) => (
{i + 1}
updateChild(i, "nameAr", v)} rtl /> updateChild(i, "id", v)} /> updateChild(i, "birthDate", v)} /> updateChild(i, "kind", v)} options={[ { value: "son", label: "Fils · ابن" }, { value: "daughter", label: "Fille · ابنة" }, ]} />
))}
update("footerFr", v)} /> update("footerAr", v)} rtl />
); } function FormSection({ title, subtitle, children, right }) { return (

{title}

{subtitle &&
{subtitle}
}
{right}
{children}
); } function Row({ children }) { return
{children}
; } function Input({ label, value, onChange, type = "text", rtl }) { return ( ); } function SelectField({ label, value, onChange, options }) { return ( ); } function RadioGroup({ label, value, onChange, options }) { return (
{label}
{options.map(o => ( ))}
); } function ToggleSwitch({ label, checked, onChange }) { return ( ); } // ===================================================================== // TWEAKS // ===================================================================== function TweaksUI({ tweaks, setTweak }) { const { TweaksPanel, TweakSection, TweakRadio, TweakToggle, TweakSelect } = window; return ( setTweak("theme", v)} options={[ { value: "teal", label: "Teal" }, { value: "plum", label: "Plum" }, { value: "olive", label: "Olive" }, { value: "ink", label: "Ink (mono)" }, { value: "rose", label: "Rose" }, ]} /> setTweak("pattern", v)} options={[ { value: "octagram", label: "Octagramme" }, { value: "weave", label: "Tressage" }, { value: "rings", label: "Anneaux" }, { value: "lines", label: "Hachures" }, ]} /> setTweak("layout", v)} options={[ { value: "classic", label: "Classique" }, { value: "compact", label: "Compact" }, ]} /> setTweak("showLogo", v)} /> setTweak("showWatermark", v)} /> ); } // ===================================================================== // APP // ===================================================================== function App() { const [data, setData] = useState(initialData); const [tweaks, setTweak] = window.useTweaks ? window.useTweaks(TWEAKS_DEFAULTS) : [TWEAKS_DEFAULTS, () => {}]; const handlePrint = () => window.print(); // Generic helper to download a card element as PNG const downloadCardAsPNG = async (selector, suffix) => { const el = document.querySelector(selector); if (!el) return; if (!window.htmlToImage) { alert("Bibliothèque html-to-image non chargée. Vérifiez votre connexion internet."); return; } try { const original = el.getAttribute("style") || ""; el.setAttribute("style", original + "; width: 600px !important; height: 950px !important; transform: none !important;" ); const dataUrl = await window.htmlToImage.toPng(el, { pixelRatio: 2, backgroundColor: "#ffffff", width: 600, height: 950, canvasWidth: 600, canvasHeight: 950, skipFonts: false, cacheBust: true, }); if (original) el.setAttribute("style", original); else el.removeAttribute("style"); const link = document.createElement("a"); const fname = (data.lastNameLat || "carte") + "_" + (data.firstNameLat || "amo") + "_" + suffix + ".png"; link.download = fname.toLowerCase().replace(/\s+/g, "_"); link.href = dataUrl; link.click(); } catch (err) { console.error(err); alert("Erreur lors du téléchargement: " + err.message); } }; const handleDownloadPNG = () => downloadCardAsPNG(".card-compact", "amo_recto"); const handleDownloadBackPNG = () => downloadCardAsPNG(".card-back", "amo_verso"); const handleReset = () => { if (confirm("Effacer tous les champs ?")) { setData({ cardNumber: "", cin: "", idcs: "", lastNameLat: "", firstNameLat: "", lastNameAr: "", firstNameAr: "", birthDate: "", issueDate: "", titulaireGender: "male", footerFr: "", footerAr: "", hasSpouse: false, spouse: { nameAr: "", id: "", birthDate: "" }, children: [], }); } }; return (
Aperçu — Recto / Verso
{1 + (data.hasSpouse ? 1 : 0) + data.children.length} bénéficiaire(s)
Recto · الوجه الأمامي
Verso · الوجه الخلفي
); } ReactDOM.createRoot(document.getElementById("root")).render();