/* 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" }}>
onQuick(p)} className="fx-quick fx-shine" style={{ position: "absolute", left: "50%", bottom: 14, transform: "translateX(-50%) translateY(10px)", zIndex: 5, opacity: 0, fontSize: 13, padding: "8px 18px", borderRadius: 9999, border: "none", background: "var(--brand-dark-900)", color: "#fff", cursor: "pointer", whiteSpace: "nowrap", transition: "opacity 220ms ease, transform 220ms ease", pointerEvents: "auto" }}>Quick view
{p.catLabel}
{p.title}
{p.rating.toFixed(1)} · {p.reviews.toLocaleString()}
${p.price}
{p.oldPrice ? ${p.oldPrice} : null}
onQuick(p)} className="fx-view-btn fx-btn-lift" style={{ position: "relative", zIndex: 2, fontSize: 14, fontWeight: 400, padding: "9px 18px", borderRadius: 9999, border: "1px solid var(--primary)", background: "#fff", color: "var(--primary)", cursor: "pointer", fontFamily: "var(--font-sans)", transition: "background 180ms ease, color 180ms ease, box-shadow 180ms ease" }}>View
{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}
{added
?
: }
);
}
/* ---- 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 (
);
}
/* ---- 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 (
{ setActive(options.findIndex((o) => o.v === value)); setOpen((o) => !o); }}
aria-haspopup="listbox"
aria-expanded={open}
className="fx-btn-lift"
style={{
display: "flex", alignItems: "center", gap: 10, width: "100%",
fontFamily: "var(--font-sans)", fontSize: "var(--body-md-size)", color: "var(--ink)",
background: "#fff", border: "1px solid " + (open ? "var(--primary)" : "var(--hairline)"),
borderRadius: 9999, padding: "10px 16px", cursor: "pointer", outline: "none",
boxShadow: open ? "0 0 0 3px rgba(83,58,253,0.14)" : "0 1px 3px rgba(0,55,112,0.05)",
transition: "border-color 180ms ease, box-shadow 180ms ease",
}}
>
Sort
{current.l}
{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 setFilter(f.key)} className="fx-filter" style={{ position: "relative", fontSize: "var(--body-md-size)", padding: "8px 16px", borderRadius: 9999, border: "1px solid " + (on ? "var(--primary)" : "var(--hairline)"), background: on ? "var(--primary)" : "#fff", color: on ? "#fff" : "var(--ink-secondary)", cursor: "pointer", transition: "all 200ms ease" }}>{f.label} ;
})}
{list.length === 0 &&
No products match your search.
}
);
}
/* ---- Categories ---- */
function Categories({ onPick }) {
return (
);
}
/* ---- 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 (
{/* ---- top: two-column offer ---- */}
{/* left column */}
Best Value — Limited Launch Offer
The Complete Fluvex Digital Toolkit
Get every premium FluvexDesigns digital product in one powerful bundle made for business, content, productivity, career, students, planning, and professional design.
{perks.map((f) => (
{check}{f}
))}
{/* price card */}
$29.99
$128+
Save 75% Today
Secure checkout · Instant access after purchase · Digital files only
All products are digital downloads. No physical product will be shipped.
Launch offer ends in:
{[[t.d, "Days"], [t.h, "Hours"], [t.m, "Minutes"], [t.s, "Seconds"]].map(([v, l]) => (
))}
{/* right column — floating collage (safe-bounded) */}
{collageCards.map((c, i) => {
const Mock = window.MOCKS[c.mock];
return (
);
})}
{labels.map((l, i) => (
{l.txt}
))}
{/* ---- included products strip ---- */}
Included in this bundle
13 premium digital products included
{bundleItems.map((it, i) => {
const Mock = window.MOCKS[it.mock];
return (
{it.name}
{check}Included
);
})}
Limited-time launch offer. Price may increase soon.
);
}
/* ---- 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 (
onQuick(window.PRODUCTS[i % window.PRODUCTS.length])} className="fx-shine" style={{ fontSize: 13, padding: "9px 18px", borderRadius: 9999, border: "1px solid rgba(255,255,255,0.4)", background: "rgba(255,255,255,0.12)", color: "#fff", backdropFilter: "blur(6px)", cursor: "pointer" }}>Preview Product
);
})}
);
}
/* ---- 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.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}
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 });