// @ds-adherence-ignore -- zelfstandige CMS-tool (eigen chrome/kleuren by design)
/* global React */
/*
  cms.jsx — Content-beheer voor de portfolio.
  - DEFAULT_CONTENT : fallback-inhoud zolang de API laadt (identiek aan api/defaults.php).
  - RT              : rendert *cursief*, **vet** en ~grijs~ als opmaak.
  - CMSPanel        : beveiligd bewerkpaneel (login → secties → opslaan in MySQL via PHP).
  Alles wordt naar window geëxporteerd zodat app.jsx het kan gebruiken.
*/

const DEFAULT_CONTENT = {
  brand: { initials: "JR", suffix: "/dev", name: "Jelle Romijn", role: "Web Developer" },
  nav: { work: "Work", about: "About", skills: "Skills", contact: "Contact ↗" },
  hero: {
    portrait: null,
    masthead: ["**Portfolio** — '26", "Software Developer in opleiding", "Grafisch Lyceum Utrecht"],
    status: "Open to new projects",
    titleLine1: "Jelle",
    titleLine2: "Romijn",
    lead: "**Software Developer** in opleiding aan het Grafisch Lyceum Utrecht. Ik bouw *werkende producten* voor echte opdrachtgevers, geen oefenprojecten voor school.",
    facts: ["**Full stack — van database tot productie**", "Grafisch Lyceum Utrecht", "PHP · Node.js · MySQL · Stripe · PWA"],
    ctaPrimary: "Bekijk werk",
    ctaGhost: "Neem contact op",
    currently: "**Nu:** ik werk aan de webshop voor Via Mercato en zet ik mijn projecten netjes in dit portfolio.",
  },
  about: {
    index: "01 — About",
    big: "Ik bouw producten die *echt werken,* ~van de database tot de livegang.~",
    approachHeading: "Zo werk ik",
    approach: [
      "Eerst het probleem snappen, dan pas bouwen.",
      "Elke keuze kan ik uitleggen: welk probleem, welke techniek en waarom.",
      "Ik maak het af en zet het live.",
    ],
    sign: "— Jelle",
    paragraphs: [
      "Ik ben **Jelle**, Software Developer in opleiding aan het Grafisch Lyceum Utrecht. Ik bouw werkende producten voor **echte opdrachtgevers**, geen oefenprojecten voor school.",
      "Ik werk full stack: de frontend, de backend in PHP, MySQL en Node.js, en ik zet alles live op Hostinger met HTTPS. Betalingen regel ik met **Stripe, iDEAL en Wero**. Naast de techniek vind ik het belangrijk om uit te leggen waarom ik bepaalde keuzes maak.",
    ],
    facts: [
      ["Opleiding", "Software Developer — Grafisch Lyceum Utrecht"],
      ["Stack", "Full stack — front-end, back-end, database"],
      ["Deployments", "Live op Hostinger via HTTPS"],
      ["Beschikbaar", "Voor echte opdrachten"],
    ],
  },
  work: { index: "02 — Selected work", title: "Projecten" },
  skillsMeta: { index: "03 — Capabilities", title: "Tech & tooling" },
  contact: {
    index: "04 — Contact",
    clockCity: "Rotterdam",
    intro: "Een mailtje is het snelst:",
    email: "hello@jelleromijn.dev",
    meta: [
      ["Beschikbaar", "Vanaf Q3 2026"],
      ["Reactietijd", "Meestal < 24 uur"],
      ["Werkwijze", "Remote · af en toe op locatie"],
    ],
    note: "Heb je een idee en wil je even sparren? Stuur gerust een bericht, ook als het nog maar een ruwe schets is.",
    socials: [
      ["Email", "mailto:hello@jelleromijn.dev"],
      ["GitHub", "#top"],
      ["LinkedIn", "#top"],
      ["Read.cv", "#top"],
    ],
    footerLeft: "© 2026 Jelle Romijn",
    footerRight: "Ontworpen & gebouwd in Rotterdam",
  },
  projects: [
    {
      title: "Via Mercato", year: "2025",
      blurb: "Webshop voor importeur van Italiaanse olijfolie — database, productfoto's, logo & teksten",
      tags: ["PHP", "MySQL", "Stripe", "iDEAL", "Wero"],
      description: "Webshop voor Via Mercato, een importeur van Italiaanse olijfolie. Ik bouwde de database, bewerkte de productfoto's, ontwierp het logo en schreef de teksten. Betalingen lopen via Stripe met iDEAL en Wero.",
      role: "Database, productfoto's, logo, teksten, frontend & backend",
      stack: ["PHP", "MySQL", "Stripe", "iDEAL", "Wero", "Adobe tools"],
      github: null, url: "https://viamercato.nl/", image: null, featured: true,
    },
    {
      title: "❤️U Festival-app", year: "2025",
      blurb: "PWA met Service Workers, push notificaties, offline werking en QR-code installatie",
      tags: ["PWA", "Next.js", "Service Workers", "Push API"],
      description: "PWA voor het ❤️U festival, een tweedaags studentenevenement op Grasweide Strijkviertel in Utrecht. De app werkt offline, stuurt pushmeldingen en installeer je via een QR-code. Bezoekers schakelen tussen Nederlands en Engels.",
      role: "Full stack — frontend, backend, kaart, push-integratie",
      stack: ["Next.js", "App Router", "next-pwa", "JavaScript", "CSS", "PHP", "MySQL", "Notifications API", "Leaflet.js", "Hostinger"],
      github: null, url: "https://festival.jelleromijn.com/", image: null, featured: false,
    },
    {
      title: "Pesten (multiplayer)", year: "2024",
      blurb: "Real-time spel via WebSockets — telefoons als spelershand, laptop als gedeeld scherm",
      tags: ["WebSockets", "Node.js", "JavaScript", "Real-time"],
      description: "Multiplayer kaartspel Pesten. Spelers gebruiken hun telefoon als controller en de laptop als speeltafel. De verbinding loopt realtime.",
      role: "Full stack — game logic, real-time verbinding",
      stack: ["Node.js", "WebSockets", "JavaScript"],
      github: null, url: "https://u240903.gluwebsite.nl/remotecontroller/", image: null, featured: false,
    },
    {
      title: "HungryHerbivore", year: "2024",
      blurb: "Kiosk-bestelsysteem met touchbediening voor grote schermen",
      tags: ["PHP", "JavaScript", "Kiosk", "Touch UI"],
      description: "Kiosk-bestelsysteem met touchbediening. Klanten stellen hun bestelling zelf samen op een groot scherm.",
      role: "Full stack — frontend, backend, bestelflow",
      stack: ["PHP", "JavaScript"],
      github: null, url: "https://u240903.gluwebsite.nl/kiosk", image: null, featured: false,
    },
  ],
  skills: [
    { heading: "Frontend", items: [["HTML / CSS", "expert"], ["JavaScript", "expert"], ["React", "gevorderd"], ["PWA / Service Workers", "gevorderd"], ["WebSockets", "gevorderd"]] },
    { heading: "Backend & Data", items: [["PHP", "gevorderd"], ["Node.js", "gevorderd"], ["MySQL", "gevorderd"], ["REST APIs", "gevorderd"], ["Hostinger / HTTPS", "gevorderd"]] },
    { heading: "Integraties & Tools", items: [["Stripe + iDEAL + Wero", "gevorderd"], ["Git / GitHub", "gevorderd"], ["Figma", "gevorderd"], ["Push Notifications API", "gevorderd"], ["QR-code flows", "gevorderd"]] },
  ],
};

const __clone = (o) => (typeof structuredClone === "function" ? structuredClone(o) : JSON.parse(JSON.stringify(o)));

/* ---------------- rich text: *cursief* **vet** ~grijs~ ---------------- */
function RT({ text }) {
  if (text == null) return null;
  const str = String(text);
  const out = [];
  const re = /(\*\*[^*]+\*\*|\*[^*]+\*|~[^~]+~)/g;
  let last = 0, m, k = 0;
  while ((m = re.exec(str)) !== null) {
    if (m.index > last) out.push(str.slice(last, m.index));
    const tok = m[0];
    if (tok.startsWith("**")) out.push(<b key={k++}>{tok.slice(2, -2)}</b>);
    else if (tok.startsWith("*")) out.push(<em className="ital" key={k++}>{tok.slice(1, -1)}</em>);
    else out.push(<span className="dim" key={k++}>{tok.slice(1, -1)}</span>);
    last = m.index + tok.length;
  }
  if (last < str.length) out.push(str.slice(last));
  return <>{out}</>;
}

/* ============================ CMS PANEL ============================ */
const { useState, useEffect, useRef, useCallback } = React;

const CMS_STYLE = `
  .cms-scrim{position:fixed;inset:0;z-index:2147483644;background:rgba(10,9,12,.32);
    -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:cmsfade .2s ease}
  @keyframes cmsfade{from{opacity:0}to{opacity:1}}

  .cms{position:fixed;top:0;right:0;bottom:0;z-index:2147483645;width:min(460px,100vw);
    display:flex;flex-direction:column;background:#1b1a20;color:#ecebef;
    font:13px/1.45 ui-sans-serif,system-ui,-apple-system,sans-serif;
    box-shadow:-16px 0 50px rgba(0,0,0,.42);animation:cmsslide .26s cubic-bezier(.22,.7,.3,1)}
  @keyframes cmsslide{from{transform:translateX(100%)}to{transform:translateX(0)}}

  .cms-hd{display:flex;align-items:center;justify-content:space-between;gap:10px;
    padding:16px 18px;border-bottom:1px solid rgba(255,255,255,.08)}
  .cms-hd h2{margin:0;font-size:15px;font-weight:700;letter-spacing:.01em}
  .cms-hd p{margin:2px 0 0;font-size:11.5px;color:#9a98a4}
  .cms-x{appearance:none;border:0;background:rgba(255,255,255,.07);color:#cfcdd6;
    width:30px;height:30px;border-radius:8px;cursor:pointer;font-size:15px}
  .cms-x:hover{background:rgba(255,255,255,.14)}

  .cms-tabs{display:flex;gap:4px;padding:10px 12px;overflow-x:auto;
    border-bottom:1px solid rgba(255,255,255,.08);scrollbar-width:none}
  .cms-tabs::-webkit-scrollbar{display:none}
  .cms-tab{appearance:none;border:0;background:transparent;color:#a6a4b0;
    padding:7px 12px;border-radius:8px;cursor:pointer;font:600 12px/1 inherit;white-space:nowrap}
  .cms-tab:hover{background:rgba(255,255,255,.06);color:#ecebef}
  .cms-tab[data-on="1"]{background:#ecebef;color:#1b1a20}

  .cms-body{flex:1;min-height:0;overflow-y:auto;padding:16px 18px 28px;
    display:flex;flex-direction:column;gap:14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.18) transparent}
  .cms-body::-webkit-scrollbar{width:9px}
  .cms-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:5px;border:2px solid transparent;background-clip:content-box}

  .cms-fld{display:flex;flex-direction:column;gap:5px}
  .cms-fld>label{font-size:11px;font-weight:600;letter-spacing:.02em;color:#b9b7c2;text-transform:uppercase}
  .cms-fld .hint{font-size:10.5px;color:#827f8c;text-transform:none;letter-spacing:0;font-weight:500}
  .cms-input,.cms-area{width:100%;box-sizing:border-box;background:#26242d;color:#f2f1f5;
    border:1px solid rgba(255,255,255,.1);border-radius:9px;padding:9px 11px;font:inherit;outline:none}
  .cms-input:focus,.cms-area:focus{border-color:#7b78ff;background:#2b2934}
  .cms-area{resize:vertical;min-height:64px;line-height:1.5}

  .cms-card{background:#211f28;border:1px solid rgba(255,255,255,.08);border-radius:13px;
    padding:14px;display:flex;flex-direction:column;gap:11px}
  .cms-card-hd{display:flex;align-items:center;justify-content:space-between;gap:8px}
  .cms-card-hd b{font-size:12.5px}
  .cms-mini{display:flex;gap:5px}
  .cms-iconbtn{appearance:none;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);
    color:#cfcdd6;width:28px;height:28px;border-radius:8px;cursor:pointer;font-size:13px;line-height:1}
  .cms-iconbtn:hover{background:rgba(255,255,255,.12)}
  .cms-iconbtn.danger:hover{background:#c4413a;border-color:#c4413a;color:#fff}
  .cms-iconbtn:disabled{opacity:.35;cursor:default}

  .cms-row{display:flex;gap:8px;align-items:center}
  .cms-row .cms-input{flex:1;min-width:0}
  .cms-grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px}

  .cms-add{appearance:none;border:1px dashed rgba(255,255,255,.22);background:transparent;
    color:#bcbac6;padding:9px;border-radius:9px;cursor:pointer;font:600 12px/1 inherit}
  .cms-add:hover{background:rgba(255,255,255,.05);color:#fff}

  .cms-toggle{display:flex;align-items:center;gap:9px;cursor:pointer;user-select:none}
  .cms-toggle .sw{position:relative;width:36px;height:20px;border-radius:999px;background:rgba(255,255,255,.16);transition:background .15s;flex:none}
  .cms-toggle .sw i{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .15s}
  .cms-toggle[data-on="1"] .sw{background:#41c971}
  .cms-toggle[data-on="1"] .sw i{transform:translateX(16px)}
  .cms-toggle span{font-size:12.5px;color:#cfcdd6}

  .cms-img{display:flex;gap:12px;align-items:center}
  .cms-img .prev{width:84px;height:84px;border-radius:10px;object-fit:cover;background:#15141a;
    border:1px solid rgba(255,255,255,.1);flex:none}
  .cms-img .prev.empty{display:grid;place-items:center;color:#6f6c79;font-size:11px;text-align:center;padding:6px}
  .cms-img .acts{display:flex;flex-direction:column;gap:6px}
  .cms-btn{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);
    color:#ecebef;padding:7px 12px;border-radius:8px;cursor:pointer;font:600 12px/1 inherit;text-align:center}
  .cms-btn:hover{background:rgba(255,255,255,.13)}
  .cms-btn.link{border:none;background:none;color:#8e8bff;padding:4px 0}

  .cms-ft{display:flex;align-items:center;gap:8px;padding:12px 16px;
    border-top:1px solid rgba(255,255,255,.08);background:#19181e}
  .cms-save{flex:1;appearance:none;border:0;border-radius:10px;cursor:pointer;
    padding:11px;font:700 13px/1 inherit;color:#0c0b10;background:#c8ff5a}
  .cms-save:hover{background:#d4ff7a}
  .cms-save:disabled{opacity:.55;cursor:default}
  .cms-ghost{appearance:none;border:1px solid rgba(255,255,255,.16);background:transparent;
    color:#cfcdd6;border-radius:10px;padding:11px 14px;cursor:pointer;font:600 12px/1 inherit}
  .cms-ghost:hover{background:rgba(255,255,255,.08)}
  .cms-status{padding:0 16px 12px;font-size:11.5px;color:#9a98a4;background:#19181e}
  .cms-status[data-kind="ok"]{color:#7ee08f}
  .cms-status[data-kind="err"]{color:#ff8a82}

  .cms-login{flex:1;display:flex;flex-direction:column;justify-content:center;gap:14px;padding:0 30px}
  .cms-login h3{margin:0;font-size:17px}
  .cms-login p{margin:0;color:#9a98a4;font-size:12.5px}
  .cms-sec-note{font-size:11px;color:#827f8c;background:rgba(255,255,255,.04);
    border:1px solid rgba(255,255,255,.07);border-radius:9px;padding:9px 11px}
  .cms-sec-note code{color:#c8ff5a;font-family:ui-monospace,monospace}
`;

const API = {
  status: () => fetch("api/auth.php", { credentials: "same-origin" }).then((r) => r.json()),
  login: (password) =>
    fetch("api/auth.php", {
      method: "POST", credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: "login", password }),
    }).then((r) => r.json()),
  logout: () =>
    fetch("api/auth.php", {
      method: "POST", credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: "logout" }),
    }).then((r) => r.json()),
  save: (content) =>
    fetch("api/content.php", {
      method: "POST", credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(content),
    }),
  load: () => fetch("api/content.php", { credentials: "same-origin" }).then((r) => r.json()),
  upload: (file) => {
    const fd = new FormData();
    fd.append("image", file);
    return fetch("api/upload.php", { method: "POST", credentials: "same-origin", body: fd }).then((r) => r.json());
  },
};

/* ---- kleine veld-helpers ---- */
function Field({ label, hint, children }) {
  return (
    <div className="cms-fld">
      <label>{label}{hint && <span className="hint"> · {hint}</span>}</label>
      {children}
    </div>
  );
}
function Text({ label, hint, value, onChange, placeholder }) {
  return (
    <Field label={label} hint={hint}>
      <input className="cms-input" value={value ?? ""} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </Field>
  );
}
function Area({ label, hint, value, onChange, rows = 3 }) {
  return (
    <Field label={label} hint={hint}>
      <textarea className="cms-area" rows={rows} value={value ?? ""}
                onChange={(e) => onChange(e.target.value)} />
    </Field>
  );
}
function Toggle({ label, value, onChange }) {
  return (
    <div className="cms-toggle" data-on={value ? "1" : "0"} onClick={() => onChange(!value)} role="switch" aria-checked={!!value}>
      <span className="sw"><i /></span><span>{label}</span>
    </div>
  );
}

// Lijst van losse tekstregels (bv. masthead, facts, tags, stack)
function StrList({ label, hint, items, onChange, placeholder }) {
  const list = items || [];
  const set = (i, v) => { const n = [...list]; n[i] = v; onChange(n); };
  const move = (i, d) => { const n = [...list]; const j = i + d; if (j < 0 || j >= n.length) return; [n[i], n[j]] = [n[j], n[i]]; onChange(n); };
  return (
    <Field label={label} hint={hint}>
      {list.map((v, i) => (
        <div className="cms-row" key={i}>
          <input className="cms-input" value={v} placeholder={placeholder} onChange={(e) => set(i, e.target.value)} />
          <button className="cms-iconbtn" title="Omhoog" disabled={i === 0} onClick={() => move(i, -1)}>↑</button>
          <button className="cms-iconbtn" title="Omlaag" disabled={i === list.length - 1} onClick={() => move(i, 1)}>↓</button>
          <button className="cms-iconbtn danger" title="Verwijder" onClick={() => onChange(list.filter((_, k) => k !== i))}>✕</button>
        </div>
      ))}
      <button className="cms-add" onClick={() => onChange([...list, ""])}>+ Regel toevoegen</button>
    </Field>
  );
}

// Lijst van paren [a, b] (bv. about-facts, contact-meta, socials, skill-items)
function PairList({ label, hint, items, onChange, ph = ["", ""] }) {
  const list = items || [];
  const set = (i, j, v) => { const n = list.map((p) => [...p]); n[i][j] = v; onChange(n); };
  const move = (i, d) => { const n = list.map((p) => [...p]); const k = i + d; if (k < 0 || k >= n.length) return; [n[i], n[k]] = [n[k], n[i]]; onChange(n); };
  return (
    <Field label={label} hint={hint}>
      {list.map((p, i) => (
        <div className="cms-row" key={i}>
          <input className="cms-input" value={p[0]} placeholder={ph[0]} onChange={(e) => set(i, 0, e.target.value)} />
          <input className="cms-input" value={p[1]} placeholder={ph[1]} onChange={(e) => set(i, 1, e.target.value)} />
          <button className="cms-iconbtn" title="Omhoog" disabled={i === 0} onClick={() => move(i, -1)}>↑</button>
          <button className="cms-iconbtn danger" title="Verwijder" onClick={() => onChange(list.filter((_, k) => k !== i))}>✕</button>
        </div>
      ))}
      <button className="cms-add" onClick={() => onChange([...list, ["", ""]])}>+ Toevoegen</button>
    </Field>
  );
}

// Verkleint/comprimeert grote rasterafbeeldingen in de browser vóór de upload,
// zodat je screenshots van elk formaat kunt kiezen én de site snel blijft.
// SVG's, GIF's en al kleine bestanden gaan ongewijzigd door.
function prepareImage(file) {
  const MAX_DIM = 2000;            // langste zijde in px
  const MAX_PASSTHROUGH = 1_200_000; // ≤ ~1,2 MB en niet te groot? laat met rust
  if (!file.type.startsWith("image/") || file.type === "image/svg+xml" || file.type === "image/gif") {
    return Promise.resolve(file);
  }
  return new Promise((resolve) => {
    const url = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => {
      URL.revokeObjectURL(url);
      const { width: w, height: h } = img;
      const tooBig = Math.max(w, h) > MAX_DIM;
      if (!tooBig && file.size <= MAX_PASSTHROUGH) { resolve(file); return; }
      const scale = tooBig ? MAX_DIM / Math.max(w, h) : 1;
      const cw = Math.round(w * scale), ch = Math.round(h * scale);
      const canvas = document.createElement("canvas");
      canvas.width = cw; canvas.height = ch;
      canvas.getContext("2d").drawImage(img, 0, 0, cw, ch);
      canvas.toBlob((blob) => {
        if (blob && blob.size < file.size) {
          const base = file.name.replace(/\.\w+$/, "") || "afbeelding";
          resolve(new File([blob], base + ".webp", { type: "image/webp" }));
        } else {
          resolve(file); // re-encode leverde niets op → origineel gebruiken
        }
      }, "image/webp", 0.85);
    };
    img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
    img.src = url;
  });
}

function ImageField({ label, value, onChange, onMsg }) {
  const inputRef = useRef(null);
  const [busy, setBusy] = useState(false);
  const pick = () => inputRef.current && inputRef.current.click();
  const onFile = async (e) => {
    const raw = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!raw) return;
    setBusy(true);
    try {
      const file = await prepareImage(raw);
      const res = await API.upload(file);
      if (res && res.path) { onChange(res.path); onMsg && onMsg("Foto geüpload", "ok"); }
      else onMsg && onMsg("Upload mislukt: " + (res.error || "onbekend"), "err");
    } catch (err) {
      onMsg && onMsg("Upload mislukt", "err");
    } finally { setBusy(false); }
  };
  return (
    <Field label={label}>
      <div className="cms-img">
        {value
          ? <img className="prev" src={value} alt="" />
          : <div className="prev empty">geen<br />foto</div>}
        <div className="acts">
          <input ref={inputRef} type="file" accept="image/*" style={{ display: "none" }} onChange={onFile} />
          <button className="cms-btn" onClick={pick} disabled={busy}>{busy ? "Bezig…" : value ? "Vervangen" : "Foto kiezen"}</button>
          {value && <button className="cms-btn link" onClick={() => onChange(null)}>Verwijderen</button>}
        </div>
      </div>
    </Field>
  );
}

/* ---- sectie-editors ---- */
function GeneralTab({ c, upd }) {
  return (
    <>
      <div className="cms-card">
        <div className="cms-card-hd"><b>Merk / logo</b></div>
        <div className="cms-grid2">
          <Text label="Initialen" value={c.brand.initials} onChange={(v) => upd((d) => { d.brand.initials = v; })} />
          <Text label="Achtervoegsel" value={c.brand.suffix} onChange={(v) => upd((d) => { d.brand.suffix = v; })} />
        </div>
        <Text label="Naam" value={c.brand.name} onChange={(v) => upd((d) => { d.brand.name = v; })} />
        <Text label="Rol" value={c.brand.role} onChange={(v) => upd((d) => { d.brand.role = v; })} />
      </div>
      <div className="cms-card">
        <div className="cms-card-hd"><b>Navigatie</b></div>
        <div className="cms-grid2">
          <Text label="Work" value={c.nav.work} onChange={(v) => upd((d) => { d.nav.work = v; })} />
          <Text label="About" value={c.nav.about} onChange={(v) => upd((d) => { d.nav.about = v; })} />
          <Text label="Skills" value={c.nav.skills} onChange={(v) => upd((d) => { d.nav.skills = v; })} />
          <Text label="Contact" value={c.nav.contact} onChange={(v) => upd((d) => { d.nav.contact = v; })} />
        </div>
      </div>
    </>
  );
}

function HeroTab({ c, upd, onMsg }) {
  const h = c.hero;
  return (
    <div className="cms-card">
      <div className="cms-card-hd"><b>Hero</b></div>
      <ImageField label="Profielfoto (op de startpagina)" value={h.portrait} onMsg={onMsg} onChange={(v) => upd((d) => { d.hero.portrait = v; })} />
      <Text label="Status-badge" value={h.status} onChange={(v) => upd((d) => { d.hero.status = v; })} />
      <div className="cms-grid2">
        <Text label="Titel regel 1" value={h.titleLine1} onChange={(v) => upd((d) => { d.hero.titleLine1 = v; })} />
        <Text label="Titel regel 2" value={h.titleLine2} onChange={(v) => upd((d) => { d.hero.titleLine2 = v; })} />
      </div>
      <Area label="Intro-tekst" hint="*cursief* **vet**" value={h.lead} onChange={(v) => upd((d) => { d.hero.lead = v; })} />
      <StrList label="Masthead-regels" hint="links bovenin" items={h.masthead} onChange={(v) => upd((d) => { d.hero.masthead = v; })} />
      <StrList label="Feiten" hint="onder de intro" items={h.facts} onChange={(v) => upd((d) => { d.hero.facts = v; })} />
      <div className="cms-grid2">
        <Text label="Knop primair" value={h.ctaPrimary} onChange={(v) => upd((d) => { d.hero.ctaPrimary = v; })} />
        <Text label="Knop secundair" value={h.ctaGhost} onChange={(v) => upd((d) => { d.hero.ctaGhost = v; })} />
      </div>
      <Area label="'Nu —' kaartje" value={h.currently} onChange={(v) => upd((d) => { d.hero.currently = v; })} />
    </div>
  );
}

function AboutTab({ c, upd }) {
  const a = c.about;
  return (
    <div className="cms-card">
      <div className="cms-card-hd"><b>Over mij</b></div>
      <Text label="Sectielabel" value={a.index} onChange={(v) => upd((d) => { d.about.index = v; })} />
      <Area label="Grote kop" hint="*cursief* ~grijs~" value={a.big} onChange={(v) => upd((d) => { d.about.big = v; })} />
      {a.paragraphs.map((p, i) => (
        <Area key={i} label={"Paragraaf " + (i + 1)} hint="**vet** *cursief*" value={p}
              onChange={(v) => upd((d) => { d.about.paragraphs[i] = v; })} />
      ))}
      <Text label="Kop 'Zo werk ik'" value={a.approachHeading} onChange={(v) => upd((d) => { d.about.approachHeading = v; })} />
      <StrList label="Werkwijze-stappen" items={a.approach} onChange={(v) => upd((d) => { d.about.approach = v; })} />
      <Text label="Ondertekening" value={a.sign} onChange={(v) => upd((d) => { d.about.sign = v; })} />
      <PairList label="Feiten-lijst" items={a.facts} ph={["Label", "Waarde"]} onChange={(v) => upd((d) => { d.about.facts = v; })} />
    </div>
  );
}

function ProjectsTab({ c, upd, onMsg }) {
  const list = c.projects || [];
  const move = (i, dir) => upd((d) => { const j = i + dir; if (j < 0 || j >= d.projects.length) return; [d.projects[i], d.projects[j]] = [d.projects[j], d.projects[i]]; });
  const add = () => upd((d) => { d.projects.push({ title: "Nieuw project", year: "2026", blurb: "", tags: [], description: "", role: "", stack: [], github: null, url: "", image: null, featured: false }); });
  return (
    <>
      <div className="cms-card">
        <div className="cms-card-hd"><b>Sectie</b></div>
        <div className="cms-grid2">
          <Text label="Sectielabel" value={c.work.index} onChange={(v) => upd((d) => { d.work.index = v; })} />
          <Text label="Titel" value={c.work.title} onChange={(v) => upd((d) => { d.work.title = v; })} />
        </div>
      </div>
      {list.map((p, i) => (
        <div className="cms-card" key={i}>
          <div className="cms-card-hd">
            <b>{String(i + 1).padStart(2, "0")} · {p.title || "Naamloos"}</b>
            <div className="cms-mini">
              <button className="cms-iconbtn" title="Omhoog" disabled={i === 0} onClick={() => move(i, -1)}>↑</button>
              <button className="cms-iconbtn" title="Omlaag" disabled={i === list.length - 1} onClick={() => move(i, 1)}>↓</button>
              <button className="cms-iconbtn danger" title="Verwijder" onClick={() => upd((d) => { d.projects.splice(i, 1); })}>✕</button>
            </div>
          </div>
          <ImageField label="Projectfoto" value={p.image} onMsg={onMsg} onChange={(v) => upd((d) => { d.projects[i].image = v; })} />
          <div className="cms-grid2">
            <Text label="Titel" value={p.title} onChange={(v) => upd((d) => { d.projects[i].title = v; })} />
            <Text label="Jaar" value={p.year} onChange={(v) => upd((d) => { d.projects[i].year = v; })} />
          </div>
          <Toggle label="Uitgelicht (★ Featured)" value={p.featured} onChange={(v) => upd((d) => { d.projects[i].featured = v; })} />
          <Area label="Korte omschrijving" hint="op de kaart" rows={2} value={p.blurb} onChange={(v) => upd((d) => { d.projects[i].blurb = v; })} />
          <Area label="Volledige omschrijving" hint="in het venster" value={p.description} onChange={(v) => upd((d) => { d.projects[i].description = v; })} />
          <Text label="Rol" value={p.role} onChange={(v) => upd((d) => { d.projects[i].role = v; })} />
          <div className="cms-grid2">
            <Text label="Website-URL" value={p.url} onChange={(v) => upd((d) => { d.projects[i].url = v; })} />
            <Text label="GitHub-URL" value={p.github || ""} onChange={(v) => upd((d) => { d.projects[i].github = v || null; })} />
          </div>
          <StrList label="Tags" hint="op de kaart" items={p.tags} onChange={(v) => upd((d) => { d.projects[i].tags = v; })} />
          <StrList label="Stack" hint="in het venster" items={p.stack} onChange={(v) => upd((d) => { d.projects[i].stack = v; })} />
        </div>
      ))}
      <button className="cms-add" onClick={add}>+ Project toevoegen</button>
    </>
  );
}

function SkillsTab({ c, upd }) {
  const list = c.skills || [];
  return (
    <>
      <div className="cms-card">
        <div className="cms-card-hd"><b>Sectie</b></div>
        <div className="cms-grid2">
          <Text label="Sectielabel" value={c.skillsMeta.index} onChange={(v) => upd((d) => { d.skillsMeta.index = v; })} />
          <Text label="Titel" value={c.skillsMeta.title} onChange={(v) => upd((d) => { d.skillsMeta.title = v; })} />
        </div>
      </div>
      {list.map((s, i) => (
        <div className="cms-card" key={i}>
          <div className="cms-card-hd">
            <b>{s.heading || "Kolom"}</b>
            <button className="cms-iconbtn danger" title="Verwijder kolom" onClick={() => upd((d) => { d.skills.splice(i, 1); })}>✕</button>
          </div>
          <Text label="Kop" value={s.heading} onChange={(v) => upd((d) => { d.skills[i].heading = v; })} />
          <PairList label="Vaardigheden" hint="niveau wordt niet getoond op de site" items={s.items} ph={["Naam", "Niveau (privé)"]} onChange={(v) => upd((d) => { d.skills[i].items = v; })} />
        </div>
      ))}
      <button className="cms-add" onClick={() => upd((d) => { d.skills.push({ heading: "Nieuwe kolom", items: [] }); })}>+ Kolom toevoegen</button>
    </>
  );
}

function ContactTab({ c, upd }) {
  const k = c.contact;
  return (
    <div className="cms-card">
      <div className="cms-card-hd"><b>Contact</b></div>
      <Text label="Sectielabel" value={k.index} onChange={(v) => upd((d) => { d.contact.index = v; })} />
      <div className="cms-grid2">
        <Text label="Stad (klok)" value={k.clockCity} onChange={(v) => upd((d) => { d.contact.clockCity = v; })} />
        <Text label="E-mail" value={k.email} onChange={(v) => upd((d) => { d.contact.email = v; })} />
      </div>
      <Text label="Intro" value={k.intro} onChange={(v) => upd((d) => { d.contact.intro = v; })} />
      <PairList label="Meta-blokken" items={k.meta} ph={["Label", "Waarde"]} onChange={(v) => upd((d) => { d.contact.meta = v; })} />
      <Area label="Notitie" value={k.note} onChange={(v) => upd((d) => { d.contact.note = v; })} />
      <PairList label="Socials" items={k.socials} ph={["Naam", "Link"]} onChange={(v) => upd((d) => { d.contact.socials = v; })} />
      <div className="cms-grid2">
        <Text label="Footer links" value={k.footerLeft} onChange={(v) => upd((d) => { d.contact.footerLeft = v; })} />
        <Text label="Footer rechts" value={k.footerRight} onChange={(v) => upd((d) => { d.contact.footerRight = v; })} />
      </div>
    </div>
  );
}

const TABS = [
  ["algemeen", "Algemeen", GeneralTab],
  ["hero", "Hero", HeroTab],
  ["about", "Over", AboutTab],
  ["projecten", "Projecten", ProjectsTab],
  ["skills", "Skills", SkillsTab],
  ["contact", "Contact", ContactTab],
];

function CMSPanel({ content, onChange, onReload }) {
  const isAdminPage = typeof window !== "undefined" && window.PORTFOLIO_ADMIN;
  const [open, setOpen] = useState(() => !!isAdminPage);
  const [authed, setAuthed] = useState(false);
  const [checking, setChecking] = useState(true);
  const [pw, setPw] = useState("");
  const [tab, setTab] = useState("algemeen");
  const [saving, setSaving] = useState(false);
  const [status, setStatus] = useState(null); // {msg, kind}

  const msg = useCallback((m, kind) => setStatus({ msg: m, kind }), []);

  // Openen via ?edit / ?admin of #edit (of automatisch op admin.html)
  useEffect(() => {
    const wantsEdit = /[?&](edit|admin)\b/.test(location.search) || location.hash === "#edit";
    if (wantsEdit) setOpen(true);
  }, []);

  useEffect(() => {
    if (!open) return;
    document.body.classList.add("reveal-done"); // edits altijd zichtbaar houden
    setChecking(true);
    API.status().then((r) => setAuthed(!!r.authed)).catch(() => {}).finally(() => setChecking(false));
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  const upd = useCallback((mutator) => {
    const next = __clone(content);
    mutator(next);
    onChange(next);
  }, [content, onChange]);

  const doLogin = async () => {
    setStatus(null);
    try {
      const r = await API.login(pw);
      if (r.authed) { setAuthed(true); setPw(""); msg("Ingelogd", "ok"); }
      else msg("Onjuist wachtwoord", "err");
    } catch { msg("Inloggen mislukt", "err"); }
  };

  const doSave = async () => {
    setSaving(true); setStatus(null);
    try {
      const res = await API.save(content);
      if (res.status === 401) { setAuthed(false); msg("Sessie verlopen — log opnieuw in", "err"); }
      else if (res.ok) {
        const j = await res.json().catch(() => ({}));
        if (j.ok) msg("Opgeslagen in de database ✓", "ok");
        else msg("Opslaan mislukt: " + (j.message || j.error || "onbekend"), "err");
      } else msg("Opslaan mislukt (" + res.status + ")", "err");
    } catch { msg("Opslaan mislukt — draait de server?", "err"); }
    finally { setSaving(false); }
  };

  const doRevert = async () => {
    try {
      const data = await API.load();
      if (data && !data.error) { onReload(data); msg("Teruggezet naar opgeslagen versie", "ok"); }
    } catch { msg("Herladen mislukt", "err"); }
  };

  const doLogout = async () => {
    try { await API.logout(); } catch {}
    setAuthed(false); msg("Uitgelogd", "ok");
  };

  if (!open) {
    // FAB-knop bewust verborgen; CMS opent via ?edit / ?admin / #edit of admin.html
    return null;
  }

  const ActiveTab = (TABS.find((t) => t[0] === tab) || TABS[0])[2];

  return (
    <>
      <style>{CMS_STYLE}</style>
      <div className="cms-scrim" onClick={() => setOpen(false)} />
      <aside className="cms" role="dialog" aria-label="Content beheren">
        <div className="cms-hd">
          <div>
            <h2>Content beheren</h2>
            <p>
              {authed ? "Wijzigingen verschijnen direct in de preview" : "Log in om te bewerken"}
              {" · "}
              <a href="studio.html" target="_blank" rel="noopener" style={{ color: "#8e8bff", textDecoration: "none" }}>Content Studio ↗</a>
            </p>
          </div>
          <button className="cms-x" onClick={() => setOpen(false)} aria-label="Sluiten">✕</button>
        </div>

        {checking ? (
          <div className="cms-login"><p>Even controleren…</p></div>
        ) : !authed ? (
          <div className="cms-login">
            <h3>Inloggen</h3>
            <p>Voer het admin-wachtwoord in om de portfolio te bewerken.</p>
            <input className="cms-input" type="password" value={pw} placeholder="Wachtwoord"
                   onChange={(e) => setPw(e.target.value)}
                   onKeyDown={(e) => { if (e.key === "Enter") doLogin(); }} autoFocus />
            <button className="cms-save" style={{ background: "#c8ff5a" }} onClick={doLogin}>Inloggen</button>
            {status && <div className="cms-status" data-kind={status.kind}>{status.msg}</div>}
            <div className="cms-sec-note">
              Standaardwachtwoord staat in <code>api/config.php</code> (of de env-variabele <code>ADMIN_PASSWORD</code>). Verander dit vóór productie.
            </div>
          </div>
        ) : (
          <>
            <div className="cms-tabs">
              {TABS.map(([id, label]) => (
                <button key={id} className="cms-tab" data-on={tab === id ? "1" : "0"} onClick={() => setTab(id)}>{label}</button>
              ))}
            </div>
            <div className="cms-body">
              <ActiveTab c={content} upd={upd} onMsg={msg} />
            </div>
            {status && <div className="cms-status" data-kind={status.kind}>{status.msg}</div>}
            <div className="cms-ft">
              <button className="cms-save" onClick={doSave} disabled={saving}>{saving ? "Opslaan…" : "Opslaan"}</button>
              <button className="cms-ghost" onClick={doRevert} title="Terug naar opgeslagen versie">Herstel</button>
              <button className="cms-ghost" onClick={doLogout}>Uitloggen</button>
            </div>
          </>
        )}
      </aside>
    </>
  );
}

Object.assign(window, { DEFAULT_CONTENT, RT, CMSPanel });
// Herbruikbare bouwstenen zodat een tweede schil (studio.jsx) dezelfde editors
// kan gebruiken met een andere vormgeving.
window.CMS = { API, clone: __clone, TABS, Field, Text, Area, Toggle, StrList, PairList, ImageField };
