// Watt Spot — Public-web client screens (read-only catalog) + admin login.
//
// Public web is a catalog only:
//   • No cart, no checkout, no devis (quote request).
//   • No favorites, no consumer signup, no consumer account.
//   • Pro clients place orders inside the Watt Spot mobile (Flutter) app.
//
// The single auth-related screen kept here is `LoginPage`, used by the
// hidden `#/admin-login` route so the admin can reach the back-office.
const { useState: useSt2, useEffect: useEf2 } = React;

// ---------- Description parser ----------
// Scraped/imported descriptions arrive as one long line with " -" separators
// (and embedded "Key : Value" specs). This turns that goo into a structured
// shape we can render as: intro paragraph + bullet list + specs table +
// outro paragraph. The product detail screen renders each section cleanly.
const parseDescription = (raw, productNom) => {
  if (!raw || typeof raw !== 'string') return null;
  const text = raw.replace(/[ \t]+/g, ' ').trim();

  // Try multiple separator strategies — scrapes/imports use very different
  // conventions. We pick the first split that produces ≥2 chunks.
  const splitters = [
    // 1. ASCII dash + Unicode dashes preceded by space
    /\s+[-–—](?=\s*[A-Za-zÀ-ÖØ-öø-ÿ0-9])/,
    // 2. Bullet chars: • · ▪ ►
    /\s*[•·▪►]\s*/,
    // 3. Newlines (manual descriptions)
    /\n+/,
  ];
  let parts = [];
  for (const re of splitters) {
    const candidate = text.split(re).map(s => s.trim()).filter(Boolean);
    if (candidate.length > 1) { parts = candidate; break; }
  }

  // 4. Last resort: split before any "Capital + lowercase words : " boundary.
  //    Catches inline spec keys even when we don't know the label, e.g.
  //    "Puissance : 12W Tension de fonctionnement : 220V Flux lumineux : 1080lm".
  //    Constraint: key starts with uppercase, runs 2–35 chars of letters
  //    and inner spaces, ends at " : " followed by a non-space character.
  if (parts.length <= 1) {
    const labelBoundary = /\s+(?=[A-ZÀ-Ý][A-Za-zÀ-ÿ'’]+(?:\s+[a-zà-ÿA-ZÀ-Ý'’]+){0,4}\s*:\s*\S)/g;
    const candidate = text.split(labelBoundary).map(s => s.trim()).filter(Boolean);
    if (candidate.length > 1) parts = candidate;
  }

  if (parts.length <= 1) {
    return { intro: text, bullets: [], specs: [], outro: '' };
  }

  // Drop a leading chunk that's just the product title repeated (common with
  // PDF scrapes — first line is the title in all-caps).
  if (productNom) {
    const titleNorm = productNom.replace(/\s+/g, ' ').trim().toLowerCase();
    if (parts[0].replace(/\s+/g, ' ').trim().toLowerCase() === titleNorm) parts.shift();
  }

  const specKeyHint = /^(marque|modèle|modele|type|puissance|type d'?ampoule|durée|duree|indice|dimensions|alimentation|économie|economie|ampoule|référence|reference|tension|couleur|finition|matière|matiere|garantie|origine|fabricant|installation|usage|fréquence|frequence|culot|flux|temperature|température|ip\d+)\b/i;

  const result = { intro: '', bullets: [], specs: [], outro: '' };
  let specMode = false;
  for (let i = 0; i < parts.length; i++) {
    const chunk = parts[i];
    // Section break: "Caractéristiques :", "Specs", "Caractéristiques techniques"
    if (/^(caractéristiques|caracteristiques|specs|specifications?)\s*[:.]?$/i.test(chunk)) {
      specMode = true; continue;
    }
    // Key : Value detection — split on the FIRST colon only.
    const m = chunk.match(/^([^:]{1,40}?)\s*:\s*(.+)$/);
    const looksLikeSpec = m && (specMode || specKeyHint.test(m[1].trim()));
    if (looksLikeSpec) {
      specMode = true;
      result.specs.push({ k: m[1].trim(), v: m[2].trim() });
      continue;
    }
    // Outro: closing CTA after the specs ("Commandez maintenant…", "Profitez de…")
    if (specMode && /^(commandez|découvrez|profitez|achetez|équipez|équipez-vous|adoptez)/i.test(chunk)) {
      result.outro = result.outro ? result.outro + ' ' + chunk : chunk;
      continue;
    }
    if (!result.intro) result.intro = chunk;
    else result.bullets.push(chunk);
  }
  return result;
};

// Renders the parsed description as: intro paragraph + bulleted highlights
// + a specs table (Caractéristiques) + closing CTA. Prefers the persisted
// `description_parsed` shape when present (admin parses on save) and only
// falls back to live parsing for old rows that haven't been re-saved yet.
const StructuredDescription = ({ raw, productNom, parsed }) => {
  // Persisted parser output? Trust it as-is — admin can also edit the
  // structured shape directly in the future.
  let d = (parsed && typeof parsed === 'object')
    ? {
        intro:   parsed.intro   || '',
        bullets: Array.isArray(parsed.bullets) ? parsed.bullets : [],
        specs:   Array.isArray(parsed.specs)   ? parsed.specs   : [],
        outro:   parsed.outro   || '',
      }
    : parseDescription(raw, productNom);
  if (!d) return null;
  // If parsing produced almost nothing structured, render the raw text.
  const hasStructure = d.bullets.length || d.specs.length || d.outro;
  if (!hasStructure) {
    return <p style={{ whiteSpace: 'pre-wrap', lineHeight: 1.7, fontSize: 14 }}>{d.intro || raw}</p>;
  }
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18, maxWidth: 820 }}>
      {d.intro && (
        <p style={{ lineHeight: 1.75, fontSize: 14.5, margin: 0 }}>{d.intro}</p>
      )}
      {d.bullets.length > 0 && (
        <div>
          <h4 style={{ fontSize: 14, margin: '0 0 10px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
            Points clés
          </h4>
          <ul style={{ margin: 0, paddingLeft: 20, lineHeight: 1.75, fontSize: 14 }}>
            {d.bullets.map((b, i) => (
              <li key={i} style={{ marginBottom: 6 }}>{b}</li>
            ))}
          </ul>
        </div>
      )}
      {d.specs.length > 0 && (
        <div>
          <h4 style={{ fontSize: 14, margin: '0 0 10px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
            Caractéristiques
          </h4>
          <div style={{ border: '1px solid var(--border)', overflow: 'hidden' }}>
            {d.specs.map((s, i) => (
              <div key={i} style={{
                display: 'grid',
                gridTemplateColumns: '180px 1fr',
                fontSize: 13.5,
                background: i % 2 === 0 ? 'var(--bg-2)' : 'transparent',
                borderTop: i > 0 ? '1px solid var(--border)' : 0,
              }}>
                <div style={{ padding: '10px 14px', color: 'var(--muted)', fontWeight: 600 }}>{s.k}</div>
                <div style={{ padding: '10px 14px', color: 'var(--fg)' }}>{s.v}</div>
              </div>
            ))}
          </div>
        </div>
      )}
      {d.outro && (
        <p style={{ lineHeight: 1.7, fontSize: 14, margin: 0, padding: 12, background: 'var(--accent-soft)', borderLeft: '3px solid var(--accent)' }}>
          {d.outro}
        </p>
      )}
    </div>
  );
};

// ---------- Specs table: uses product.specs (jsonb) + always-known fields ----------
const SpecTable = ({ p, cat }) => {
  const specs = p.specs && typeof p.specs === 'object' ? p.specs : {};
  const rows = [
    ['Marque', p.marque],
    ['Référence', p.ref],
    ['Catégorie', cat?.nom],
    ...Object.entries(specs),
  ].filter(([, v]) => v != null && v !== '');
  if (rows.length === 0) {
    return <p className="ws-muted">Aucune caractéristique renseignée.</p>;
  }
  return (
    <table className="ws-spec-table">
      <tbody>
        {rows.map(([k, v]) => (
          <tr key={k}><th>{k}</th><td>{String(v)}</td></tr>
        ))}
      </tbody>
    </table>
  );
};

// ---------- Reviews section: read-only on the public web ----------
// Visitors can read pro-customer reviews left from the mobile app, but the
// public site has no auth, so it cannot accept new submissions.
const ReviewsSection = ({ p }) => {
  const [reviews, setReviews] = useSt2([]);
  const [loading, setLoading] = useSt2(true);

  useEf2(() => {
    let alive = true;
    setLoading(true);
    window.WS_DB.listReviews(p.id)
      .then(r => { if (alive) setReviews(r || []); })
      .catch(() => {})
      .finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [p.id]);

  const timeAgo = (iso) => {
    if (!iso) return '';
    const d = new Date(iso);
    const diff = Math.floor((Date.now() - d.getTime()) / 1000);
    if (diff < 60) return 'à l\'instant';
    if (diff < 3600) return `il y a ${Math.floor(diff/60)} min`;
    if (diff < 86400) return `il y a ${Math.floor(diff/3600)} h`;
    if (diff < 2592000) return `il y a ${Math.floor(diff/86400)} j`;
    return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' });
  };

  const total = reviews.length;
  const buckets = [5,4,3,2,1].map(n => {
    const c = reviews.filter(r => r.rating === n).length;
    return { n, c, pct: total ? Math.round((c / total) * 100) : 0 };
  });
  const avg = total ? (reviews.reduce((a, r) => a + r.rating, 0) / total).toFixed(1) : '—';

  return (
    <div className="ws-avis">
      <div className="ws-avis-summary">
        <div className="ws-avis-big"><strong>{avg}</strong><span>/5</span></div>
        <div className="ws-avis-bars">
          {buckets.map(b => (
            <div key={b.n} className="ws-avis-bar">
              <span>{b.n}★</span>
              <div className="ws-avis-track"><div style={{ width: b.pct+'%' }}/></div>
              <span className="ws-muted">{b.pct}%</span>
            </div>
          ))}
        </div>
      </div>

      <div className="ws-avis-list" style={{ marginTop: 20 }}>
        {loading && <div className="ws-muted">Chargement…</div>}
        {!loading && reviews.length === 0 && (
          <div className="ws-muted" style={{ padding: 20, textAlign: 'center' }}>
            Aucun avis pour ce produit pour l'instant.
          </div>
        )}
        {reviews.map(r => {
          const name = r.author_name || 'Client';
          const initial = name[0] || '?';
          return (
            <div key={r.id} className="ws-avis-item">
              <div className="ws-avis-head">
                <div className="ws-avatar">{initial}</div>
                <div><strong>{name}</strong><div className="ws-muted">{timeAgo(r.created_at)}</div></div>
                <div style={{ marginLeft: 'auto' }}>
                  {[1,2,3,4,5].map(i => <Icon key={i} name="star" size={12} style={{ color: i<=r.rating?'var(--accent)':'var(--border)', fill: i<=r.rating?'var(--accent)':'none' }}/>)}
                </div>
              </div>
              {r.title && <div style={{ fontWeight: 600, marginBottom: 4 }}>{r.title}</div>}
              {r.body && <p style={{ whiteSpace: 'pre-wrap' }}>{r.body}</p>}
            </div>
          );
        })}
      </div>
    </div>
  );
};

// ---------- Zoomable product image ----------
// Hover magnifies the image under the cursor within the same frame (like a
// loupe); click opens the fullscreen Lightbox for a clear, full-size view.
const ZoomImage = ({ src, alt, onOpen }) => {
  const [hover, setHover] = useSt2(false);
  const [pos, setPos] = useSt2({ x: 50, y: 50 });

  const onMove = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 100;
    const y = ((e.clientY - rect.top) / rect.height) * 100;
    setPos({ x: Math.max(0, Math.min(100, x)), y: Math.max(0, Math.min(100, y)) });
  };

  return (
    <div
      style={{ width: '100%', height: '100%', cursor: 'zoom-in' }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onMouseMove={onMove}
      onClick={onOpen}
    >
      <img src={src} alt={alt} style={{
        width: '100%', height: '100%', objectFit: 'cover',
        transform: hover ? 'scale(1.8)' : 'scale(1)',
        transformOrigin: `${pos.x}% ${pos.y}%`,
        transition: hover ? 'none' : 'transform 0.15s ease-out',
      }}/>
    </div>
  );
};

// ---------- Fullscreen image lightbox ----------
const ImageLightbox = ({ images, index, onClose, onNav }) => {
  useEf2(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight') onNav((index + 1) % images.length);
      if (e.key === 'ArrowLeft') onNav((index - 1 + images.length) % images.length);
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [index, images.length]);

  return (
    <div className="ws-lightbox" onClick={onClose}>
      <button className="ws-lightbox-close" onClick={onClose} title="Fermer"><Icon name="close" size={20}/></button>
      {images.length > 1 && (
        <button className="ws-lightbox-nav ws-lightbox-prev"
          onClick={(e) => { e.stopPropagation(); onNav((index - 1 + images.length) % images.length); }}>
          <Icon name="chevL" size={22}/>
        </button>
      )}
      <img src={images[index]} alt="" className="ws-lightbox-img" onClick={(e) => e.stopPropagation()}/>
      {images.length > 1 && (
        <button className="ws-lightbox-nav ws-lightbox-next"
          onClick={(e) => { e.stopPropagation(); onNav((index + 1) % images.length); }}>
          <Icon name="chevR" size={22}/>
        </button>
      )}
      {images.length > 1 && (
        <div className="ws-lightbox-count">{index + 1} / {images.length}</div>
      )}
    </div>
  );
};

// ---------- Product Detail (read-only) ----------
const ProductDetail = ({ id, onNav }) => {
  const [p, setP] = useSt2(null);
  const [cats, setCats] = useSt2(window.CATEGORIES || []);
  const [related, setRelated] = useSt2([]);
  const [tab, setTab] = useSt2('desc');
  const [activeImg, setActiveImg] = useSt2(0);
  const [lightboxOpen, setLightboxOpen] = useSt2(false);

  useEf2(() => {
    let alive = true;
    setP(null);
    setActiveImg(0);
    Promise.all([
      window.WS_DB.getProduct(id),
      window.WS_DB.listCategories(),
    ]).then(([prod, cs]) => {
      if (!alive) return;
      // Defend against direct links into a product hidden from the web.
      if (prod && prod.web_visible === false) {
        setP(null);
        return;
      }
      setP(prod || null);
      if (cs && cs.length) setCats(cs);
      if (prod) {
        window.WS_DB.listProducts({ category: prod.cat, platform: 'web' })
          .then(list => { if (alive) setRelated((list || []).filter(x => x.id !== prod.id).slice(0, 4)); })
          .catch(() => {});
      }
    }).catch(() => {});
    return () => { alive = false; };
  }, [id]);

  if (!p) {
    return (
      <div style={{ padding: 80, textAlign: 'center' }} className="ws-muted">
        <Icon name="loader" size={32} style={{ opacity: 0.4 }}/>
        <div style={{ marginTop: 12 }}>Chargement du produit…</div>
      </div>
    );
  }

  const cat = cats.find(c => c.id === p.cat);

  return (
    <div className="ws-pd">
      <div className="ws-breadcrumb">
        <a onClick={()=>onNav('home')}>Accueil</a>
        <Icon name="chevR" size={12}/>
        <a onClick={()=>onNav('cat', { cat: p.cat })}>{cat?.nom}</a>
        <Icon name="chevR" size={12}/>
        <span>{p.nom}</span>
      </div>

      <div className="ws-pd-grid">
        <div className="ws-pd-gallery">
          <div className="ws-pd-main">
            {(() => {
              const imgs = [p.image_url, ...(p.images || [])].filter(Boolean);
              const current = imgs[activeImg];
              if (current) {
                return <ZoomImage src={current} alt={p.nom} onOpen={() => setLightboxOpen(true)}/>;
              }
              return <Thumb p={p} size={480}/>;
            })()}
            {p.tag && <span className={`ws-chip ws-chip-${p.tag.toLowerCase().replace(/[^a-z]/g,'')}`} style={{ position: 'absolute', top: 16, left: 16 }}>{p.tag}</span>}
          </div>
          <div className="ws-pd-thumbs">
            {(() => {
              const imgs = [p.image_url, ...(p.images || [])].filter(Boolean);
              if (imgs.length === 0) {
                return [0,1,2,3].map(i => (
                  <div key={i} className={`ws-pd-thumb ${i===0?'active':''}`}><Thumb p={p}/></div>
                ));
              }
              return imgs.map((url, i) => (
                <div
                  key={i}
                  className={`ws-pd-thumb ${i===activeImg?'active':''}`}
                  onClick={()=>setActiveImg(i)}
                  style={{ cursor: 'pointer' }}
                >
                  <img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                </div>
              ));
            })()}
          </div>
        </div>

        <div className="ws-pd-info">
          <div className="ws-pd-meta">
            <span>{p.marque}</span>
            <span>·</span>
            <span>Réf {p.ref}</span>
          </div>
          <h1>{p.nom}</h1>
          {p.note ? (
            <div className="ws-prod-note" style={{ marginTop: 8 }}>
              {[1,2,3,4,5].map(i => (
                <Icon key={i} name="star" size={14} style={{ color: i <= Math.round(p.note) ? 'var(--accent)' : 'var(--border)', fill: i <= Math.round(p.note) ? 'var(--accent)' : 'none' }}/>
              ))}
              <span><strong>{p.note}</strong></span>
              <span className="ws-muted">· {p.avis} avis</span>
            </div>
          ) : null}

          <div style={{ marginTop: 8 }}><StockBadge status={p.stockStatus}/></div>

          <div className="ws-pd-assurance">
            <div><Icon name="map" size={18}/><div><strong>Showroom Manouba</strong><span>44 Av. Habib Bougatfa</span></div></div>
            <div><Icon name="shield" size={18}/><div><strong>Garantie 2 ans</strong><span>Toutes marques</span></div></div>
            <div><Icon name="check" size={18}/><div><strong>Conforme NF/CE</strong><span>Normes tunisiennes</span></div></div>
          </div>
        </div>
      </div>

      {/* Tabs */}
      <div className="ws-pd-tabs">
        <div className="ws-tab-nav">
          <button className={tab==='desc'?'active':''} onClick={()=>setTab('desc')}>Description</button>
          <button className={tab==='spec'?'active':''} onClick={()=>setTab('spec')}>Caractéristiques</button>
          <button className={tab==='avis'?'active':''} onClick={()=>setTab('avis')}>Avis ({p.avis || 0})</button>
        </div>
        <div className="ws-tab-body">
          {tab === 'desc' && (
            <div className="ws-pd-desc">
              {p.desc
                ? <StructuredDescription raw={p.desc} productNom={p.nom} parsed={p.description_parsed}/>
                : <p className="ws-muted">Aucune description pour ce produit.</p>
              }
            </div>
          )}
          {tab === 'spec' && (
            <SpecTable p={p} cat={cat}/>
          )}
          {tab === 'avis' && (
            <ReviewsSection p={p}/>
          )}
        </div>
      </div>

      <section className="ws-section">
        <div className="ws-section-head"><h2>Produits similaires</h2></div>
        <div className="ws-prod-grid">
          {related.map(x => <ProductCard key={x.id} p={x} onOpen={() => onNav('product', { id: x.id })}/>)}
        </div>
      </section>

      {lightboxOpen && (() => {
        const imgs = [p.image_url, ...(p.images || [])].filter(Boolean);
        if (!imgs.length) return null;
        return <ImageLightbox images={imgs} index={activeImg} onClose={() => setLightboxOpen(false)} onNav={setActiveImg}/>;
      })()}
    </div>
  );
};

// ---------- Validation helpers (kept for admin login) ----------
const validators = {
  email: v => {
    if (!v || !v.trim()) return 'Email requis';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim())) return 'Email invalide';
    return '';
  },
  password: v => {
    if (!v) return 'Mot de passe requis';
    if (v.length < 6) return 'Minimum 6 caractères';
    return '';
  },
  required: (v, label) => {
    if (!v || !String(v).trim()) return `${label} requis`;
    return '';
  },
};

// ---------- Admin login page (only entry to the back-office) ----------
// Reached via the hidden URL `#/admin-login`. There is no consumer signup,
// no password recovery, no "quick order" — those belong to the mobile app
// (Pro clients) or to Supabase admin tooling.
const LoginPage = ({ onNav, onLogin }) => {
  const [form, setForm] = useSt2({ email: '', password: '' });
  const [errs, setErrs] = useSt2({});
  const [busy, setBusy] = useSt2(false);
  const [globalErr, setGlobalErr] = useSt2('');

  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const validate = () => {
    const e = {
      email: validators.email(form.email),
      password: validators.password(form.password),
    };
    const cleaned = Object.fromEntries(Object.entries(e).filter(([,v]) => v));
    setErrs(cleaned);
    return Object.keys(cleaned).length === 0;
  };

  const submit = async () => {
    setGlobalErr('');
    if (!validate()) return;
    setBusy(true);
    try {
      let user = await window.WS_DB.login({ email: form.email, password: form.password });
      try {
        const fresh = await window.WS_DB.refreshSession?.();
        if (fresh) user = fresh;
      } catch (_) {}
      if (!user?.is_admin) {
        setGlobalErr("Ce compte n'a pas les droits d'administration.");
        try { await window.WS_DB.logout(); } catch (_) {}
        setBusy(false);
        return;
      }
      onLogin(user);
      onNav('admin-dashboard');
    } catch (e) {
      setGlobalErr(e.message || 'Erreur');
    } finally {
      setBusy(false);
    }
  };

  const fieldErr = (k) => errs[k] && <div style={{ color: 'var(--danger)', fontSize: 12, marginTop: 4 }}>{errs[k]}</div>;

  return (
    <div className="ws-auth">
      <div className="ws-auth-card">
        <Logo onNav={onNav}/>
        <h2 style={{ marginTop: 18 }}>Espace administrateur</h2>
        <p className="ws-muted">Connectez-vous pour accéder au tableau de bord.</p>

        <div className="ws-field" style={{ marginTop: 14 }}>
          <label>Email *</label>
          <input type="email" autoComplete="email"
                 value={form.email} onChange={e=>set('email', e.target.value)}
                 placeholder="admin@wattspot.tn"/>
          {fieldErr('email')}
        </div>
        <div className="ws-field">
          <label>Mot de passe *</label>
          <input type="password" autoComplete="current-password"
                 value={form.password} onChange={e=>set('password', e.target.value)}
                 placeholder="••••••••"/>
          {fieldErr('password')}
        </div>

        {globalErr && (
          <div style={{ background: 'rgba(220,60,60,0.08)', color: 'var(--danger)', padding: 10, fontSize: 13, margin: '10px 0', border: '1px solid var(--danger)' }}>
            {globalErr}
          </div>
        )}

        <button className="ws-btn ws-btn-primary ws-btn-lg ws-btn-full" disabled={busy} onClick={submit}>
          {busy ? '…' : 'Se connecter'} {!busy && <Icon name="chevR" size={16}/>}
        </button>

        <div style={{ textAlign: 'center', marginTop: 16 }}>
          <a className="ws-link-sm" style={{ cursor: 'pointer' }} onClick={()=>onNav('home')}>
            <Icon name="chevL" size={12}/> Retour au catalogue
          </a>
        </div>
      </div>
    </div>
  );
};

// Stubs for legacy exports — these screens were removed when the public
// web became a read-only catalog. Any leftover route just bounces home.
const goHome = (onNav) => { useEf2(() => { onNav && onNav('home'); }, []); return null; };
const CartPage          = ({ onNav }) => goHome(onNav);
const CheckoutPage      = ({ onNav }) => goHome(onNav);
const CheckoutAuthGate  = ({ onNav }) => goHome(onNav);
const AccountPage       = ({ onNav }) => goHome(onNav);
const UserOrderDetail   = ({ onBack }) => { useEf2(() => { onBack && onBack(); }, []); return null; };
const AddressEditor     = ({ onCancel }) => { useEf2(() => { onCancel && onCancel(); }, []); return null; };

Object.assign(window, { ProductDetail, CartPage, CheckoutPage, CheckoutAuthGate, LoginPage, AccountPage, AddressEditor, UserOrderDetail, SpecTable, ReviewsSection, StructuredDescription, parseDescription });
