/* store-shop.jsx — shop UI: product cards, filter bar, product grid, category grid, featured bundle, gallery, quick-view modal. Interaction handlers come from the app via props. Exported to window. */ const { Button: SBtn, Card: SCard, Tag: STag } = window.FluvexDesignsDesignSystem_072666; const { Reveal: SReveal, CountUp: SCountUp, useTilt: sUseTilt, useCountdown: sUseCountdown, Magnetic: SMag, useSpotlight: sUseSpot } = window; const { StoreIcon: SIcon } = window; const PREVIEW_BG = "linear-gradient(155deg, #f7f5ff 0%, #efe9ff 55%, #f4ecff 100%)"; function Stars({ value = 5, size = 13, color = "var(--primary)" }) { return {"★★★★★".slice(0, Math.round(value))}{"★★★★★".slice(Math.round(value))}; } function BadgePill({ children }) { if (!children) return null; return {children}; } function Heart({ on, onClick }) { return ( ); } function ProductCard({ p, onQuick, onAdd, wished, onWish, delay = 0 }) { const Mock = window.MOCKS[p.mock]; const tiltRef = sUseTilt(6, 1.04); const [added, setAdded] = React.useState(false); const doAdd = () => { onAdd(p); setAdded(true); setTimeout(() => setAdded(false), 1400); }; return (
{p.badge} onWish(p.id)} />
onQuick(p)} role="button" tabIndex={0} onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && onQuick(p)} aria-label={`View ${p.title}`} style={{ position: "absolute", inset: 0, transition: "transform 300ms ease", cursor: "pointer" }}>
{p.catLabel}

{p.title}

{p.rating.toFixed(1)} · {p.reviews.toLocaleString()}
${p.price} {p.oldPrice ? ${p.oldPrice} : null}
{p.isDemoAvailable && p.demoLink ? ( e.stopPropagation()} title="Preview this product safely" className="fx-view-btn fx-btn-lift" style={{ position: "relative", zIndex: 2, fontSize: 14, fontWeight: 400, padding: "9px 14px", borderRadius: 9999, border: "1px solid var(--hairline)", background: "#fff", color: "var(--ink)", cursor: "pointer", fontFamily: "var(--font-sans)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: 6 }}> Demo ) : null}
); } /* ---- Best sellers ---- */ function BestSellers({ onQuick, onAdd, wishes, onWish }) { const list = window.BEST_IDS.map((id) => window.PRODUCTS.find((p) => p.id === id)); const spot = sUseSpot(); return (
{list.map((p, i) => )}
); } /* ---- Custom sort dropdown (premium theme, portal-rendered) ---- The panel is rendered into document.body via ReactDOM.createPortal so it escapes ALL parent stacking contexts, transforms, overflows, and filters in the shop layout. Placement is computed from the trigger's getBoundingClientRect. */ function SortDropdown({ value, onChange }) { const options = [ { v: "featured", l: "Featured" }, { v: "new", l: "Newest" }, { v: "best", l: "Best Seller" }, { v: "low", l: "Price: Low to High" }, { v: "high", l: "Price: High to Low" }, ]; const current = options.find((o) => o.v === value) || options[0]; const [open, setOpen] = React.useState(false); const [active, setActive] = React.useState(options.findIndex((o) => o.v === value)); const [pos, setPos] = React.useState({ top: 0, left: 0, width: 220 }); const btnRef = React.useRef(null); const panelRef = React.useRef(null); const measure = React.useCallback(() => { const el = btnRef.current; if (!el) return; const r = el.getBoundingClientRect(); setPos({ top: r.bottom + 8, left: r.right - Math.max(r.width, 220), width: Math.max(r.width, 220) }); }, []); React.useEffect(() => { if (!open) return; measure(); const onDoc = (e) => { if (btnRef.current && btnRef.current.contains(e.target)) return; if (panelRef.current && panelRef.current.contains(e.target)) return; setOpen(false); }; const onKey = (e) => { if (e.key === "Escape") setOpen(false); else if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => (a + 1) % options.length); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => (a - 1 + options.length) % options.length); } else if (e.key === "Enter") { e.preventDefault(); onChange(options[active].v); setOpen(false); } }; const onScroll = () => measure(); document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); window.addEventListener("scroll", onScroll, true); window.addEventListener("resize", onScroll); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); window.removeEventListener("scroll", onScroll, true); window.removeEventListener("resize", onScroll); }; }, [open, active, onChange, options.length, measure]); const panel = open ? ReactDOM.createPortal(
{options.map((o, i) => { const selected = o.v === value; const isActive = i === active; return (
setActive(i)} onClick={() => { onChange(o.v); setOpen(false); }} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "10px 14px", borderRadius: 10, cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: "var(--body-md-size)", color: selected ? "#fff" : "var(--ink)", background: selected ? "linear-gradient(135deg,#8f79d5,#533afd)" : (isActive ? "rgba(83,58,253,0.08)" : "transparent"), boxShadow: selected ? "0 6px 14px rgba(83,58,253,0.28)" : "none", transition: "background 140ms ease, color 140ms ease", }} > {o.l} {selected ? ( ) : null}
); })}
, document.body ) : null; return (
{panel}
); } /* ---- Filterable shop grid ---- */ function Shop({ onQuick, onAdd, wishes, onWish }) { const [filter, setFilter] = React.useState("all"); const [sort, setSort] = React.useState("featured"); const [q, setQ] = React.useState(""); const shopSpot = sUseSpot(); let list = window.PRODUCTS.filter((p) => (filter === "all" || p.cat.includes(filter)) && p.title.toLowerCase().includes(q.toLowerCase())); if (sort === "low") list = [...list].sort((a, b) => a.price - b.price); else if (sort === "high") list = [...list].sort((a, b) => b.price - a.price); else if (sort === "best") list = [...list].sort((a, b) => b.reviews - a.reviews); else if (sort === "new") list = [...list].reverse(); return (
{window.FILTERS.map((f) => { const on = filter === f.key; return ; })}
setQ(e.target.value)} placeholder="Search products" style={{ border: "none", outline: "none", fontSize: "var(--body-md-size)", fontFamily: "var(--font-sans)", background: "transparent", width: 130, color: "var(--ink)" }} />
{list.map((p, i) => )}
{list.length === 0 &&

No products match your search.

}
); } /* ---- Categories ---- */ function Categories({ onPick }) { return (
{window.CATEGORIES.map((c, i) => ( onPick && onPick(c.filter)} className="fx-cat fx-card-hover" style={{ display: "flex", alignItems: "center", gap: 18, padding: 24, borderRadius: "var(--radius-xl)", border: "1px solid var(--hairline)", background: "#fff", textDecoration: "none", height: "100%" }}>
{c.title}
{c.count} products
))}
); } /* ---- Featured bundle: The Complete Fluvex Digital Toolkit ---- High-converting premium offer. EDIT copy / price / links here. Replace `checkoutLink` with your Stripe / Gumroad / Shopify / Payhip / Lemon Squeezy checkout URL. Do not put a direct download file here unless payment protection is handled by the checkout provider. */ function Bundle({ onAdd }) { const target = React.useRef(Date.now() + (2 * 864e5 + 14 * 36e5 + 37 * 6e4)).current; const t = sUseCountdown(target); const bundleSpot = sUseSpot(); const collageTilt = sUseTilt(6, 1); const paraA = window.useParallax(24); const paraB = window.useParallax(-18); const paraC = window.useParallax(32); const paraD = window.useParallax(-26); // The 13 real FluvexDesigns products included in this bundle. `mock` = collage art, `icon`/`grad` = tile art. const bundleItems = [ { name: "Editable Teacher Planner Studio 2026–2027", mock: "MkPlanner", icon: "planner", grad: "linear-gradient(135deg,#c9b6f5,#533afd)" }, { name: "Fluvex Business Card Studio", mock: "MkCard", icon: "briefcase", grad: "linear-gradient(135deg,#1c1e54,#533afd)" }, { name: "Content Calendar & Growth Dashboard", mock: "MkCalendar", icon: "grid", grad: "linear-gradient(135deg,#b9b9f9,#665efd)" }, { name: "Fluvex Life Tracker Pro", mock: "MkDashboard", icon: "sparkle", grad: "linear-gradient(135deg,#9ad1c9,#533afd)" }, { name: "Fluvex Budget Tracker Studio", mock: "MkBudget", icon: "clock", grad: "linear-gradient(135deg,#f7b986,#ea2261)" }, { name: "Wedding Invitation Studio — Rose Edition", mock: "MkWeddingBundle", icon: "heart", grad: "linear-gradient(135deg,#f96bee,#c9b6f5)" }, { name: "Editable Floral Wedding Invitation Studio", mock: "MkFloralWedding", icon: "heart", grad: "linear-gradient(135deg,#f7b986,#f96bee)" }, { name: "Fluvex Photo Resume Studio", mock: "MkResume", icon: "doc", grad: "linear-gradient(135deg,#b9b9f9,#533afd)" }, { name: "Fluvex Media Kit Studio", mock: "MkMediaKit", icon: "share", grad: "linear-gradient(135deg,#c9b6f5,#f96bee)" }, { name: "Fluvex Invoice Studio", mock: "MkInvoice", icon: "briefcase", grad: "linear-gradient(135deg,#533afd,#ea2261)" }, { name: "Fluvex Cover Letter Studio", mock: "MkCoverLetter", icon: "edit", grad: "linear-gradient(135deg,#f7e7cb,#f7b986)" }, { name: "Fluvex Resume Studio", mock: "MkResume", icon: "doc", grad: "linear-gradient(135deg,#665efd,#c9b6f5)" }, { name: "AI Academic Mastery Prompt Pack (PDF)", mock: "MkAIPromptsBundle", icon: "sparkle", grad: "linear-gradient(135deg,#665efd,#9ad1c9)", aspect: "1 / 1" }, ]; const perks = [ "Instant digital download", "Video demos included", "Editable files", "Beginner friendly", "Lifetime access", "Future updates included", "No physical product shipped", ]; const bundleProduct = { id: "complete-toolkit", title: "The Complete Fluvex Digital Toolkit", price: 29.99, productType: "HTML + PDF Bundle", // ⛑ Replace this checkoutLink with your Stripe, Gumroad, Shopify, Payhip, or Lemon Squeezy checkout link. checkoutLink: (window.BUNDLE && window.BUNDLE.checkoutLink) || "#checkout-complete-toolkit", link: (window.BUNDLE && window.BUNDLE.checkoutLink) || "#checkout-complete-toolkit", includedProducts: bundleItems.map((b) => b.name), // ⛑ Do not expose the raw ZIP here — deliver via the checkout provider after payment. bundleDownloadFile: (window.BUNDLE && window.BUNDLE.deliveryFile) || "protected-download-after-payment/Complete-Fluvex-Digital-Toolkit.zip", }; const check = ; const collageCards = [ { mock: "MkCalendar", ref: paraA, top: "2%", left: "5%", w: "46%", rot: -6, dur: "8s", d: "0s", z: 4 }, { mock: "MkDashboard", ref: paraB, top: "0%", right: "5%", w: "42%", rot: 5, dur: "7s", d: "-2s", z: 3 }, { mock: "MkWeddingBundle", ref: paraC, top: "30%", left: "26%", w: "48%", rot: 2, dur: "9.5s", d: "-3s", z: 6 }, { mock: "MkPlanner", ref: paraA, bottom: "20%",left: "2%", w: "42%", rot: 4, dur: "9s", d: "-4s", z: 5 }, { mock: "MkBudget", ref: paraD, bottom: "14%", right: "3%", w: "44%", rot: -4, dur: "8.5s", d: "-1s", z: 7 }, { mock: "MkResume", ref: paraB, bottom: "2%", left: "34%", w: "30%", rot: -3, dur: "7.5s", d: "-2.5s", z: 6 }, ]; const labels = [ { txt: "13 Products Included", top: "2%", left: "32%", dur: "6s", d: "0s" }, { txt: "Save 75%", top: "38%", right: "12%", dur: "7s", d: "-1s", hot: true }, { txt: "Instant Download", top: "58%", left: "6%", dur: "6.5s", d: "-2s", hideSm: true }, { txt: "Video Demo Included", bottom: "4%", left: "34%", dur: "7.5s", d: "-3s" }, { txt: "Limited Offer", top: "14%", right: "18%", dur: "6.8s", d: "-1.5s", hideSm: true }, ]; return (
); } /* ---- Gallery (masonry-ish) ---- */ function Gallery({ onQuick }) { const items = ["MkPlanner", "MkInvoice", "MkDashboard", "MkMediaKit", "MkResume", "MkCalendar", "MkCard", "MkBudget"]; const spans = [1, 1, 1, 1, 1, 1, 1, 1]; return (
{items.map((m, i) => { const Mock = window.MOCKS[m]; return (
); })}
); } /* ---- Quick view modal ---- */ function QuickView({ product, onClose, onAdd }) { const [show, setShow] = React.useState(false); React.useEffect(() => { const id = requestAnimationFrame(() => setShow(true)); return () => cancelAnimationFrame(id); }, []); React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, []); if (!product) return null; const Mock = window.MOCKS[product.mock]; return (
e.stopPropagation()} style={{ width: "min(860px, 100%)", maxHeight: "90vh", overflow: "auto", background: "#fff", borderRadius: "var(--radius-xl)", boxShadow: "var(--shadow-2)", display: "grid", gridTemplateColumns: "1fr 1fr", transform: show ? "scale(1) translateY(0)" : "scale(0.94) translateY(12px)", transition: "transform 300ms cubic-bezier(.21,.68,.24,1)" }} className="fx-modal">
{product.badge}
{product.catLabel}

{product.title}

{product.rating.toFixed(1)} · {product.reviews.toLocaleString()} reviews

{product.longDescription || product.shortDescription || "A premium FluvexDesigns digital product. Open in your browser or PDF reader and start using it immediately — no software required."}

{product.productType || "HTML Product"} {product.category || "Digital Product"} Instant Digital Download
What's included
    {(product.includedFiles || ["Interactive HTML file", "Works in your browser"]).map((f, i) => (
  • {f}
  • ))}
Delivery
{product.deliveryInstructions || "Instant digital download delivered by the connected checkout platform after payment."}
${product.price} {product.oldPrice ? ${product.oldPrice} : null}
{/* Buy Now → sends customer straight to the product checkoutLink. Replace with your Stripe/Gumroad/Payhip/Lemon Squeezy link in store-data.jsx. */} { if (!product.checkoutLink || product.checkoutLink.startsWith("#")) { e.preventDefault(); window.alert("Checkout link not connected yet."); } }} style={{ flex: "1 1 200px", textDecoration: "none" }}> {product.isDemoAvailable && product.demoLink ? ( View Demo ) : null}
After purchase, you will receive instant access to your digital files. No physical product will be shipped.
); } Object.assign(window, { Stars, ProductCard, BestSellers, Shop, Categories, Bundle, Gallery, QuickView });