// linktree.js — shared React components that render the public linktree.
// Used by view.html (read-only public profile) and edit.html (live preview).
// Mirrors the original stanmaxx.html design 1:1; the only deltas are:
//   • Photo + bubble thumb accept image URLs (uploaded files) and fall back to
//     the original SVG placeholders when nothing is uploaded yet.
//   • Cover style is locked to "stack" (per product spec — no toggle).
//   • Bubble is locked to top-right rounded (per product spec).
//   • Motion is locked to "lively" (~18s drift cycle).
// Exposes window.Linktree = { App, Background, Header, Identity, EventCard,
// Links, ProfileCover, BubbleSticker, PhotoPortrait, PhotoSecondary, LinkIcon }
// so other scripts can mount a preview or just reuse a piece.

const { useState, useEffect, useRef, useCallback } = React;

// ── i18n helpers ────────────────────────────────────────────────────────────
// Linktree components are mounted both on the public profile (view.html) and
// inside the editor preview (edit.html). On view.html the owner's locale is
// applied to window.i18n before this code runs. On edit.html the editor's
// own locale (= the visitor's = the owner's) is already set. So we just call
// window.i18n.t() directly.
//
// useI18n is a tiny hook that re-renders the component when the language
// changes (the i18n module dispatches "i18n:change" on window). Without it,
// React wouldn't know to re-render translated strings after setLang().
function useI18n() {
  const [, setTick] = useState(0);
  useEffect(() => {
    const onChange = () => setTick((n) => n + 1);
    window.addEventListener("i18n:change", onChange);
    return () => window.removeEventListener("i18n:change", onChange);
  }, []);
  return useCallback((key, params, fallback) => {
    if (typeof window !== "undefined" && window.i18n) {
      const v = window.i18n.t(key, params);
      if (v && v !== key) return v;
    }
    return fallback != null ? fallback : key;
  }, []);
}

// ── Background ──────────────────────────────────────────────────────────────
function Background() {
  return (
    <div className="bg" aria-hidden="true">
      <div className="bg-base" />
      <div className="bg-blob bg-blob-a" />
      <div className="bg-blob bg-blob-b" />
      <div className="bg-blob bg-blob-c" />
      <div className="bg-grain" />
    </div>
  );
}

// ── Header ──────────────────────────────────────────────────────────────────
function Header({ rightSlot, shareUrl, housePseudo, houseEnabled, markHref }) {
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [error, setError] = useState(false);
  const wrapRef = useRef(null);
  const resetRef = useRef(null);
  const t = useI18n();

  // The URL we share. Prefer an explicit shareUrl (passed by LinktreeApp so
  // the editor's preview points at /pseudo and not /edit); fall back to the
  // current page when none is provided (public profile view).
  const url = shareUrl
    || (typeof location !== "undefined" ? location.href : "");

  // Click-outside + Escape close the popover.
  useEffect(() => {
    if (!open) return;
    const onDown = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", onDown);
    document.addEventListener("touchstart", onDown, { passive: true });
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("touchstart", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  const flash = (which) => {
    if (which === "copied") { setCopied(true); setError(false); }
    else                    { setError(true); setCopied(false); }
    if (resetRef.current) clearTimeout(resetRef.current);
    resetRef.current = setTimeout(() => {
      setCopied(false); setError(false);
    }, 1800);
  };

  const doCopy = async () => {
    try {
      if (navigator.clipboard) {
        await navigator.clipboard.writeText(url);
        flash("copied");
      } else {
        // Legacy fallback for older browsers.
        const ta = document.createElement("textarea");
        ta.value = url;
        ta.style.position = "fixed";
        ta.style.opacity = "0";
        document.body.appendChild(ta);
        ta.select();
        try { document.execCommand("copy"); flash("copied"); }
        catch { flash("error"); }
        document.body.removeChild(ta);
      }
    } catch {
      flash("error");
    }
  };

  const doNativeShare = async () => {
    if (!navigator.share) { doCopy(); return; }
    try {
      await navigator.share({ url, title: document.title || "stanmaxx" });
      setOpen(false);
    } catch (e) {
      // User dismissed the native sheet — leave popover open silently.
      if (e && e.name !== "AbortError") flash("error");
    }
  };

  const canNativeShare = typeof navigator !== "undefined" && !!navigator.share;

  return (
    <header className="header">
      <a className="header-mark" href={markHref || "/"} aria-label="stanmaxx.com">
        <span className="dot" />
        <span className="header-mark-name">
          créé avec stanmaxx<span className="muted">.com</span>
        </span>
      </a>
      <div className="header-actions">
        {/* House entrance — the owner enabled a companion page, so we
            offer a one-tap jump to it from the linktree header. We use
            the same glass chip recipe as the share button and keep it
            tucked left of share so the share button stays the right
            anchor visitors know. We don't show this button on the
            editor preview (rightSlot is set there), to keep the header
            from getting too crowded inside the small preview pane. */}
        {!rightSlot && houseEnabled && housePseudo && (
          <a className="header-house" href={'/' + housePseudo + '/house'}
             aria-label={t("view.visit_house", { pseudo: housePseudo },
                          "Visiter la maison de @" + housePseudo)}>
            <svg viewBox="0 0 24 24" width="16" height="16" fill="none"
                 stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"
                 strokeLinejoin="round">
              <path d="M3 11.5L12 4l9 7.5"/>
              <path d="M5 10.5V20a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-9.5"/>
              <path d="M10 21v-6h4v6"/>
            </svg>
          </a>
        )}
        {rightSlot || (
          <div className="header-share-wrap" ref={wrapRef}>
            <button className="header-share" aria-label={t("common.share_link", null, "Partager cette page")}
                    aria-haspopup="menu"
                    aria-expanded={open ? "true" : "false"}
                    onClick={() => setOpen((v) => !v)}>
              <svg viewBox="0 0 24 24" width="16" height="16" fill="none"
                   stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"
                   strokeLinejoin="round">
                <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7" />
                <path d="M16 6l-4-4-4 4" />
                <path d="M12 2v14" />
              </svg>
            </button>

            {open && (
              <div className="share-pop" role="menu">
                <div className="share-pop-title">{t("common.share_link", null, "Partager cette page")}</div>
                <div className="share-pop-url" title={url}>{url}</div>

                <button type="button"
                        className={`share-pop-primary ${copied ? "is-copied" : ""} ${error ? "is-error" : ""}`}
                        onClick={doCopy}>
                  {copied ? (
                    <>
                      <svg viewBox="0 0 16 16" width="14" height="14" fill="none"
                           stroke="currentColor" strokeWidth="2.2"
                           strokeLinecap="round" strokeLinejoin="round">
                        <path d="M3 8.5l3.2 3L13 4.5" />
                      </svg>
                      {t("common.link_copied", null, "Lien copié")}
                    </>
                  ) : error ? (
                    t("common.error_retry", null, "Erreur — réessayer")
                  ) : (
                    <>
                      <svg viewBox="0 0 16 16" width="14" height="14" fill="none"
                           stroke="currentColor" strokeWidth="1.8"
                           strokeLinecap="round" strokeLinejoin="round">
                        <rect x="5" y="5" width="8" height="9" rx="1.5" />
                        <path d="M3 11V3.5A1.5 1.5 0 0 1 4.5 2H10" />
                      </svg>
                      {t("common.copy_link", null, "Copier le lien")}
                    </>
                  )}
                </button>

                {canNativeShare && (
                  <button type="button"
                          className="share-pop-secondary"
                          onClick={doNativeShare}>
                    <svg viewBox="0 0 24 24" width="13" height="13" fill="none"
                         stroke="currentColor" strokeWidth="1.8"
                         strokeLinecap="round" strokeLinejoin="round">
                      <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7" />
                      <path d="M16 6l-4-4-4 4" />
                      <path d="M12 2v14" />
                    </svg>
                    {t("common.more_options", null, "Plus d'options (apps)")}
                  </button>
                )}
              </div>
            )}
          </div>
        )}
      </div>
    </header>
  );
}

// ── ProfileCover ────────────────────────────────────────────────────────────
// Square Liquid-Glass frame holding the profile photo, with a corner sticker
// (BubbleSticker). Click the sticker → expand its mini photo to fill the
// cover; click anywhere to collapse. The cover is locked to "stack" style
// per the product spec.
function ProfileCover({ photoUrl, storyUrl, bubbleText, expanded, onToggle }) {
  return (
    <div className="cover cover-stack">
      <div className="cover-shadow cover-shadow-2" />
      <div className="cover-shadow cover-shadow-1" />

      <div className="cover-frame">
        <div className={`cover-photo ${expanded ? "is-hidden" : ""}`}>
          {photoUrl ? <img src={photoUrl} alt="" /> : <PhotoPortrait />}
        </div>
        <div className={`cover-photo ${expanded ? "" : "is-hidden"}`}>
          {storyUrl ? <img src={storyUrl} alt="" /> : <PhotoSecondary />}
        </div>

        <div className="cover-gloss" aria-hidden="true" />
        <div className="cover-rim" aria-hidden="true" />

        <div className="cover-chip">
          <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">
            <path d="M8 1.5l1.6 1.2 2 .1.1 2 1.2 1.6-1.2 1.6-.1 2-2 .1L8 11.3 6.4 10.1l-2-.1-.1-2L3.1 6.4l1.2-1.6.1-2 2-.1L8 1.5z"
                  fill="#fff" />
            <path d="M5.6 8.1l1.6 1.6 3.2-3.4" fill="none" stroke="currentColor"
                  strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          verified
        </div>
      </div>

      <BubbleSticker
        text={bubbleText}
        storyUrl={storyUrl}
        active={expanded}
        onClick={onToggle}
      />
    </div>
  );
}

// ── BubbleSticker ───────────────────────────────────────────────────────────
// Glass speech bubble pinned to the cover's top-right corner (locked by
// product spec). Holds bubble text + a small thumbnail of the secondary
// photo. Click → tells parent to expand.
function BubbleSticker({ text, storyUrl, active, onClick }) {
  const corner = "tr";
  const shape = "rounded";
  const t = useI18n();
  return (
    <button
      type="button"
      className={`bubble bubble-${corner} bubble-${shape} ${active ? "is-active" : ""}`}
      onClick={onClick}
      aria-pressed={active}
      aria-label={active
        ? t("linktree.photo_collapse", null, "Réduire la photo")
        : t("linktree.photo_expand",   null, "Agrandir la photo")}
    >
      <div className="bubble-thumb">
        {storyUrl ? <img src={storyUrl} alt="" /> : <PhotoSecondary />}
        {active && (
          <div className="bubble-close" aria-hidden="true">
            <svg viewBox="0 0 16 16" width="12" height="12" fill="none"
                 stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </div>
        )}
      </div>
      <div className="bubble-text">
        {String(text || "").split("\n").map((line, i) => (
          <span key={i}>{line || "\u00A0"}</span>
        ))}
      </div>
      <div className="bubble-tail" aria-hidden="true" />
    </button>
  );
}

// ── Identity ────────────────────────────────────────────────────────────────
function Identity({ displayName, handle, tagline, nameColor }) {
  // The owner-picked name color (when present) overrides the default white.
  // We always keep the drop-shadow so the text reads on bright gradients
  // even with pale colors like ivory or soft pink. nameColor comes from
  // a server-enforced allowlist, so it's safe to inject as a CSS color.
  const nameStyle = nameColor ? { color: nameColor } : undefined;
  return (
    <div className="identity">
      <h1 className="identity-name">
        <span className="identity-given" style={nameStyle}>{displayName}</span>
        <span className="identity-handle">{handle}</span>
      </h1>
      {tagline && <p className="identity-tag">{tagline}</p>}
    </div>
  );
}

// ── Event ───────────────────────────────────────────────────────────────────
function EventCard({ event }) {
  const t = useI18n();
  if (!event || !event.enabled) return null;
  return (
    <a className="event glass" href={event.href || "#"}>
      <div className="event-date">
        <span className="event-d">{event.date?.d || ""}</span>
        <span className="event-m">{event.date?.m || ""}</span>
      </div>
      <div className="event-body">
        {event.badge && (
          <span className="event-badge">
            <span className="event-pulse" />
            {event.badge}
          </span>
        )}
        <div className="event-title">{event.title}</div>
        <div className="event-sub">{event.subtitle}</div>
      </div>
      <div className="event-cta">
        {event.cta || t("linktree.event_cta_fallback", null, "Voir")}
        <svg viewBox="0 0 16 16" width="12" height="12" fill="none"
             stroke="currentColor" strokeWidth="2" strokeLinecap="round"
             strokeLinejoin="round">
          <path d="M5 3l5 5-5 5" />
        </svg>
      </div>
    </a>
  );
}

// ── House entrance ───────────────────────────────────────────────────────────
// A prominent card in the body of the public profile that invites visitors
// to step into the owner's companion house. The shareable link stays unique
// (stanmaxx.com/pseudo) — this card is how a visitor who only has that one
// link discovers and reaches the house at /pseudo/house. We only render it
// when the owner has enabled their house (data.houseEnabled). Styled as a
// glass card to match EventCard, with a cozy little house glyph so it reads
// as "a place" rather than just another link.
function HouseEntrance({ pseudo, enabled }) {
  const t = useI18n();
  if (!enabled || !pseudo) return null;
  return (
    <a className="house-entrance glass" href={`/${pseudo}/house`}>
      <div className="house-entrance-icon" aria-hidden="true">
        <svg viewBox="0 0 48 48" width="34" height="34" fill="none">
          {/* little cozy house — black outline, warm fill, lit window */}
          <path d="M8 22L24 9l16 13" stroke="currentColor" strokeWidth="2.4"
                strokeLinecap="round" strokeLinejoin="round"/>
          <path d="M11 20v17a1.5 1.5 0 0 0 1.5 1.5h23A1.5 1.5 0 0 0 37 37V20"
                stroke="currentColor" strokeWidth="2.4"
                strokeLinecap="round" strokeLinejoin="round"/>
          <rect x="20.5" y="28" width="7" height="10.5" rx="1"
                stroke="currentColor" strokeWidth="2.2"/>
          <rect x="14.5" y="24" width="5" height="5" rx="1"
                fill="currentColor" opacity=".55"/>
          <rect x="28.5" y="24" width="5" height="5" rx="1"
                fill="currentColor" opacity=".55"/>
        </svg>
      </div>
      <div className="house-entrance-body">
        <div className="house-entrance-title">
          {t("view.house_entrance_title", null, "Ma maison")}
        </div>
        <div className="house-entrance-sub">
          {t("view.house_entrance_sub", null, "Entre, laisse un mot dans la boîte aux lettres")}
        </div>
      </div>
      <div className="house-entrance-cta">
        {t("view.house_entrance_cta", null, "Entrer")}
        <svg viewBox="0 0 16 16" width="12" height="12" fill="none"
             stroke="currentColor" strokeWidth="2" strokeLinecap="round"
             strokeLinejoin="round">
          <path d="M5 3l5 5-5 5" />
        </svg>
      </div>
    </a>
  );
}

// ── Store entrance ───────────────────────────────────────────────────────────
// Sibling of HouseEntrance: a glass card on the public link page that takes a
// visitor to the owner's WhatsApp store at /:pseudo/store. Rendered only when
// the store is enabled AND the owner chose to surface it on the link page
// (storeOnLinkpage). Shows the shop's name when set, falling back to a generic
// label. The little bag/storefront glyph reads as "a shop".
function StoreEntrance({ pseudo, enabled, onLinkpage, storeName }) {
  const t = useI18n();
  if (!enabled || !onLinkpage || !pseudo) return null;
  const title = (storeName && storeName.trim())
    ? storeName
    : t("view.store_entrance_title", null, "Ma boutique");
  return (
    <a className="store-entrance glass" href={`/${pseudo}/store`}>
      <div className="store-entrance-icon" aria-hidden="true">
        <svg viewBox="0 0 48 48" width="32" height="32" fill="none">
          {/* storefront: awning + counter, black outline to match house glyph */}
          <path d="M9 18l2.4-6.5a2 2 0 0 1 1.9-1.3h21.4a2 2 0 0 1 1.9 1.3L39 18"
                stroke="currentColor" strokeWidth="2.4"
                strokeLinecap="round" strokeLinejoin="round"/>
          <path d="M11 18v18a1.6 1.6 0 0 0 1.6 1.6h22.8A1.6 1.6 0 0 0 37 36V18"
                stroke="currentColor" strokeWidth="2.4"
                strokeLinecap="round" strokeLinejoin="round"/>
          <path d="M9 18h30" stroke="currentColor" strokeWidth="2.4"
                strokeLinecap="round" strokeLinejoin="round"/>
          <rect x="20.5" y="26" width="7" height="11.6" rx="1"
                stroke="currentColor" strokeWidth="2.2"/>
          <path d="M16 18v3.5M24 18v3.5M32 18v3.5"
                stroke="currentColor" strokeWidth="1.6" opacity=".55"
                strokeLinecap="round"/>
        </svg>
      </div>
      <div className="store-entrance-body">
        <div className="store-entrance-title">{title}</div>
        <div className="store-entrance-sub">
          {t("view.store_entrance_sub", null, "Commande directement sur WhatsApp")}
        </div>
      </div>
      <div className="store-entrance-cta">
        {t("view.store_entrance_cta", null, "Voir")}
        <svg viewBox="0 0 16 16" width="12" height="12" fill="none"
             stroke="currentColor" strokeWidth="2" strokeLinecap="round"
             strokeLinejoin="round">
          <path d="M5 3l5 5-5 5" />
        </svg>
      </div>
    </a>
  );
}

// ── Links ───────────────────────────────────────────────────────────────────
function Links({ items }) {
  if (!items || items.length === 0) return null;

  // Intercept the click so we can fire a tracking ping before navigation.
  // We use sendBeacon when available — it's specifically designed for this
  // case (browser keeps the request alive across navigation) and never
  // blocks the user. Modifier-clicks (cmd, ctrl, shift, middle) bypass the
  // tracker entirely so power users opening in new tabs aren't slowed.
  const onLinkClick = (e, item) => {
    // Let the browser handle "open in new tab", "open in new window",
    // right-click, copy-link, etc. — these don't navigate the current page
    // so we don't need to delay anything. We DO still fire a beacon so
    // the click is counted even when the user opens in a new tab.
    const isNewTab = e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1;
    if (typeof item.id === "number" && item.id > 0) {
      const body = JSON.stringify({ id: item.id });
      try {
        if (navigator.sendBeacon) {
          navigator.sendBeacon("/api/track/click",
            new Blob([body], { type: "application/json" }));
        } else {
          // Old browsers — fire-and-forget fetch. keepalive lets it survive
          // the navigation (when supported).
          fetch("/api/track/click", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body,
            keepalive: true,
          }).catch(() => {});
        }
      } catch { /* tracking failures must never block navigation */ }
    }
    // Don't preventDefault — let the native click do its thing. With
    // sendBeacon the ping fires from the browser even after we unload.
    void isNewTab; // documented intent above; no further action needed
  };

  return (
    <nav className="links">
      {items.map((item) => (
        <a key={item.id ?? item.label}
           className="link glass"
           href={item.href || "#"}
           onClick={(e) => onLinkClick(e, item)}>
          <LinkIcon name={item.icon} />
          <div className="link-body">
            <div className="link-label">{item.label}</div>
            {item.sub && <div className="link-sub">{item.sub}</div>}
          </div>
          <div className="link-arrow">
            <svg viewBox="0 0 16 16" width="12" height="12" fill="none"
                 stroke="currentColor" strokeWidth="2" strokeLinecap="round"
                 strokeLinejoin="round">
              <path d="M5 3l5 5-5 5" />
            </svg>
          </div>
        </a>
      ))}
    </nav>
  );
}

function LinkIcon({ name }) {
  const wrap = (children) => (
    <div className="link-icon" aria-hidden="true">{children}</div>
  );
  switch (name) {
    case "ig":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="3" width="18" height="18" rx="5" />
          <circle cx="12" cy="12" r="4" />
          <circle cx="17.5" cy="6.5" r=".7" fill="currentColor" stroke="none" />
        </svg>
      );
    case "tk":
      return wrap(
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M14 3v10.4a3.6 3.6 0 1 1-3.6-3.6h.6V13a1.6 1.6 0 1 0 1.6 1.6V3h2.7a4.7 4.7 0 0 0 4.7 4.7V10a7 7 0 0 1-6-3.4z" />
        </svg>
      );
    case "yt":
      return wrap(
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M21.6 7.2a2.5 2.5 0 0 0-1.7-1.7C18.3 5 12 5 12 5s-6.3 0-7.9.5A2.5 2.5 0 0 0 2.4 7.2 26 26 0 0 0 2 12a26 26 0 0 0 .4 4.8 2.5 2.5 0 0 0 1.7 1.7c1.6.5 7.9.5 7.9.5s6.3 0 7.9-.5a2.5 2.5 0 0 0 1.7-1.7A26 26 0 0 0 22 12a26 26 0 0 0-.4-4.8zM10 15V9l5 3-5 3z" />
        </svg>
      );
    case "sp":
      return wrap(
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm4.7 14.4a.7.7 0 0 1-1 .2c-2.6-1.6-5.9-1.9-9.8-1a.7.7 0 0 1-.3-1.4c4.2-1 7.9-.6 10.8 1.2.3.2.4.6.3 1zm1.3-2.9a.9.9 0 0 1-1.2.3c-3-1.8-7.5-2.4-11-1.3a.9.9 0 1 1-.5-1.7c4-1.2 8.9-.6 12.4 1.5.4.2.5.8.3 1.2zm.1-3c-3.6-2.1-9.5-2.3-12.9-1.3a1.1 1.1 0 1 1-.6-2.1c4-1.2 10.5-1 14.6 1.5a1.1 1.1 0 1 1-1.1 1.9z" />
        </svg>
      );
    case "bag":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <path d="M5 8h14l-1 12H6L5 8z" />
          <path d="M9 8a3 3 0 0 1 6 0" />
        </svg>
      );
    case "mail":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="5" width="18" height="14" rx="2" />
          <path d="M3 7l9 6 9-6" />
        </svg>
      );
    case "globe":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="9" />
          <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />
        </svg>
      );
    case "music":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <path d="M9 18V6l12-2v12" />
          <circle cx="6" cy="18" r="3" />
          <circle cx="18" cy="16" r="3" />
        </svg>
      );
    case "video":
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="6" width="14" height="12" rx="2" />
          <path d="M17 10l4-2v8l-4-2z" />
        </svg>
      );
    default:
      return wrap(
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
             strokeLinecap="round" strokeLinejoin="round">
          <path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1.5 1.5" />
          <path d="M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1.5-1.5" />
        </svg>
      );
  }
}

// ── Footer ──────────────────────────────────────────────────────────────────
function Footer({ pseudo }) {
  return (
    <footer className="footer">
      <span>© {new Date().getFullYear()} · stanmaxx</span>
      {pseudo && <>
        <span className="dot-sep">·</span>
        <span>@{pseudo}</span>
      </>}
    </footer>
  );
}

// ── Photo placeholders ──────────────────────────────────────────────────────
// Two visual placeholders — primary (portrait), secondary (lifestyle).
// Each is a CSS gradient/SVG composition so the layout is convincing
// without relying on user-supplied imagery.
function PhotoPortrait() {
  return (
    <svg viewBox="0 0 400 400" preserveAspectRatio="xMidYMid slice"
         width="100%" height="100%">
      <defs>
        <linearGradient id="pp-sky" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#FFD580" />
          <stop offset=".55" stopColor="#FF8FAB" />
          <stop offset="1" stopColor="#7B2CBF" />
        </linearGradient>
        <radialGradient id="pp-sun" cx=".7" cy=".25" r=".35">
          <stop offset="0" stopColor="#FFF6D5" stopOpacity=".95" />
          <stop offset="1" stopColor="#FFF6D5" stopOpacity="0" />
        </radialGradient>
        <linearGradient id="pp-water" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#FF6F91" />
          <stop offset="1" stopColor="#3A0CA3" />
        </linearGradient>
      </defs>
      <rect width="400" height="260" fill="url(#pp-sky)" />
      <rect width="400" height="260" fill="url(#pp-sun)" />
      <circle cx="280" cy="105" r="46" fill="#FFE08A" opacity=".9" />
      <rect y="260" width="400" height="140" fill="url(#pp-water)" />
      <rect y="258" width="400" height="3" fill="#fff" opacity=".55" />
      <g fill="#1a0b1f" opacity=".88">
        <ellipse cx="200" cy="395" rx="74" ry="10" opacity=".25" />
        <path d="M200 165 q-22 0 -22 22 q0 16 12 22 q-30 12 -38 50 q-6 32 -2 138 l100 0 q4 -106 -2 -138 q-8 -38 -38 -50 q12 -6 12 -22 q0 -22 -22 -22z" />
      </g>
      <g fill="#0c0612" opacity=".85">
        <path d="M-10 0 q40 30 50 70 q-20 -20 -50 -10z" />
        <path d="M-10 0 q60 8 90 36 q-40 -6 -90 4z" />
      </g>
    </svg>
  );
}

function PhotoSecondary() {
  return (
    <svg viewBox="0 0 400 400" preserveAspectRatio="xMidYMid slice"
         width="100%" height="100%">
      <defs>
        <linearGradient id="ps-bg" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stopColor="#FF8A3D" />
          <stop offset="1" stopColor="#FF2D78" />
        </linearGradient>
        <linearGradient id="ps-pool" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#22D3EE" />
          <stop offset="1" stopColor="#0EA5C9" />
        </linearGradient>
      </defs>
      <rect width="400" height="400" fill="url(#ps-bg)" />
      <rect x="40" y="200" width="320" height="170" rx="14" fill="url(#ps-pool)" />
      <g stroke="#fff" strokeOpacity=".18" strokeWidth="1">
        <line x1="40" y1="240" x2="360" y2="240" />
        <line x1="40" y1="280" x2="360" y2="280" />
        <line x1="40" y1="320" x2="360" y2="320" />
        <line x1="120" y1="200" x2="120" y2="370" />
        <line x1="200" y1="200" x2="200" y2="370" />
        <line x1="280" y1="200" x2="280" y2="370" />
      </g>
      <ellipse cx="240" cy="260" rx="60" ry="6" fill="#fff" opacity=".5" />
      <ellipse cx="220" cy="290" rx="40" ry="3" fill="#fff" opacity=".35" />
      <g transform="translate(110 150)">
        <ellipse cx="0" cy="6" rx="50" ry="14" fill="#000" opacity=".15" />
        <ellipse cx="0" cy="0" rx="50" ry="14" fill="#FFE066" />
        <ellipse cx="0" cy="0" rx="32" ry="9" fill="url(#ps-pool)" />
      </g>
      <g transform="translate(330 70)" fill="#0c0612" opacity=".82">
        <rect x="-3" y="0" width="6" height="120" />
        <path d="M0 0 q-50 -10 -70 -40 q40 0 70 30z" />
        <path d="M0 0 q50 -10 70 -40 q-40 0 -70 30z" />
        <path d="M0 0 q-30 -40 -70 -36 q20 30 70 30z" />
        <path d="M0 0 q30 -40 70 -36 q-20 30 -70 30z" />
      </g>
    </svg>
  );
}

// ── App: full linktree page ─────────────────────────────────────────────────
// `data` is the public profile payload from /api/u/:pseudo (or /api/profile/me).
// `headerSlot` lets the editor inject a "Back to edit" button.
function LinktreeApp({ data, headerSlot }) {
  const [expanded, setExpanded] = useState(false);

  // Apply gradient settings to :root variables.
  useEffect(() => {
    if (!data) return;
    const r = document.documentElement;
    const p = data.palette || ["#FFE066", "#FF8A3D", "#FF3D7F", "#9D2BFF"];
    r.style.setProperty("--c1", p[0] || "#FFE066");
    r.style.setProperty("--c2", p[1] || "#FF8A3D");
    r.style.setProperty("--c3", p[2] || "#FF3D7F");
    r.style.setProperty("--c4", p[3] || "#9D2BFF");
    r.style.setProperty("--grad-angle", (data.gradAngle ?? 180) + "deg");
    r.style.setProperty("--grad-intensity",
      ((data.gradIntensity ?? 100) / 100).toString());
    // Motion is locked to "lively" per product spec.
    r.style.setProperty("--motion-dur", "18s");

    // Note: we intentionally do NOT set a <meta name="theme-color"> here.
    // A flat theme-color paints Safari's URL bar and Chrome's chrome with a
    // solid block of colour, which looked like an orange "band" above and
    // below the gradient on mobile. Leaving theme-color absent lets the
    // browser auto-detect the page colour and tint its chrome to match
    // (Safari ≥ 15, Chrome ≥ 130) — when auto-detect isn't available the
    // chrome falls back to a neutral translucent tone, which still reads
    // cleaner than a solid orange band.
  }, [data]);

  if (!data) {
    return (
      <div className="page">
        <Background />
      </div>
    );
  }

  return (
    <div className="page">
      <Background />
      <main className="stage">
        <Header
          rightSlot={headerSlot}
          shareUrl={`${location.origin}/${data.pseudo}`}
          housePseudo={data.pseudo}
          houseEnabled={!!data.houseEnabled}
        />

        <ProfileCover
          photoUrl={data.photoUrl}
          storyUrl={data.storyUrl}
          bubbleText={data.bubbleText || `@${data.pseudo}`}
          expanded={expanded}
          onToggle={() => setExpanded((v) => !v)}
        />

        <Identity
          displayName={data.displayName || data.pseudo}
          handle={` @stanmaxx`}
          tagline={data.tagline}
          nameColor={data.nameColor}
        />

        <EventCard event={data.event} />

        <Links items={data.links} />

        <HouseEntrance pseudo={data.pseudo} enabled={!!data.houseEnabled} />

        <StoreEntrance
          pseudo={data.pseudo}
          enabled={!!data.storeEnabled}
          onLinkpage={data.storeOnLinkpage !== false}
          storeName={data.storeName}
        />

        <Footer pseudo={data.pseudo} />
      </main>
    </div>
  );
}

// View tracking — called from view.html once the public profile data is
// available. Uses sendBeacon when possible so the ping doesn't block. The
// server enforces dedup per (visitor × profile) over 24h so calling this
// on every page load is safe; the counter only goes up once.
function trackView(pseudo) {
  if (!pseudo || typeof pseudo !== "string") return;
  try {
    const body = JSON.stringify({ pseudo });
    if (navigator.sendBeacon) {
      navigator.sendBeacon("/api/track/view",
        new Blob([body], { type: "application/json" }));
    } else {
      fetch("/api/track/view", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body,
        credentials: "same-origin",
        keepalive: true,
      }).catch(() => {});
    }
  } catch { /* tracking is best-effort */ }
}

// Expose for other scripts.
window.Linktree = {
  App: LinktreeApp,
  Background,
  Header,
  Identity,
  EventCard,
  HouseEntrance,
  StoreEntrance,
  Links,
  ProfileCover,
  BubbleSticker,
  PhotoPortrait,
  PhotoSecondary,
  LinkIcon,
  trackView,
};
