@neowhale/storefront 0.2.29 → 0.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -555,7 +555,9 @@ function useAnalytics() {
555
555
  /** Whether tracking is globally enabled for this storefront */
556
556
  trackingEnabled,
557
557
  /** Configured recording sample rate (0–1) for behavioral session replays */
558
- recordingRate: config.recordingRate
558
+ recordingRate: config.recordingRate,
559
+ /** Stable visitor ID for cross-session attribution */
560
+ visitorId: getVisitorId(config.storagePrefix)
559
561
  };
560
562
  }
561
563
  function useAuth() {
@@ -2454,6 +2456,68 @@ function useReferral() {
2454
2456
  referredBy: status?.referred_by ?? null
2455
2457
  };
2456
2458
  }
2459
+ var NUM_PATTERN = /(\$?[\d,]+\.?\d*[+★%]?)/g;
2460
+ function easeOutQuart(t) {
2461
+ return 1 - Math.pow(1 - t, 4);
2462
+ }
2463
+ function useCountUp(target, duration, start) {
2464
+ const [value, setValue] = react.useState(0);
2465
+ const raf = react.useRef(0);
2466
+ react.useEffect(() => {
2467
+ if (!start) return;
2468
+ const t0 = performance.now();
2469
+ function tick(now2) {
2470
+ const elapsed = now2 - t0;
2471
+ const progress = Math.min(elapsed / duration, 1);
2472
+ setValue(Math.round(easeOutQuart(progress) * target));
2473
+ if (progress < 1) raf.current = requestAnimationFrame(tick);
2474
+ }
2475
+ raf.current = requestAnimationFrame(tick);
2476
+ return () => cancelAnimationFrame(raf.current);
2477
+ }, [target, duration, start]);
2478
+ return value;
2479
+ }
2480
+ function AnimatedNumber({ raw }) {
2481
+ const ref = react.useRef(null);
2482
+ const [visible, setVisible] = react.useState(false);
2483
+ react.useEffect(() => {
2484
+ const el = ref.current;
2485
+ if (!el || typeof IntersectionObserver === "undefined") {
2486
+ setVisible(true);
2487
+ return;
2488
+ }
2489
+ const obs = new IntersectionObserver(([entry]) => {
2490
+ if (entry.isIntersecting) {
2491
+ setVisible(true);
2492
+ obs.disconnect();
2493
+ }
2494
+ }, { threshold: 0.3 });
2495
+ obs.observe(el);
2496
+ return () => obs.disconnect();
2497
+ }, []);
2498
+ const prefix = raw.startsWith("$") ? "$" : "";
2499
+ const suffix = raw.match(/[+★%]$/)?.[0] || "";
2500
+ const numeric = parseFloat(raw.replace(/[\$,+★%]/g, ""));
2501
+ const hasCommas = raw.includes(",");
2502
+ const decimals = raw.includes(".") ? raw.split(".")[1]?.replace(/[+★%]/g, "").length || 0 : 0;
2503
+ const count = useCountUp(
2504
+ decimals > 0 ? Math.round(numeric * Math.pow(10, decimals)) : numeric,
2505
+ 1400,
2506
+ visible
2507
+ );
2508
+ const display = decimals > 0 ? (count / Math.pow(10, decimals)).toFixed(decimals) : hasCommas ? count.toLocaleString() : String(count);
2509
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { ref, children: [
2510
+ prefix,
2511
+ display,
2512
+ suffix
2513
+ ] });
2514
+ }
2515
+ function AnimatedText({ text }) {
2516
+ const parts = text.split(NUM_PATTERN);
2517
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map(
2518
+ (part, i) => NUM_PATTERN.test(part) ? /* @__PURE__ */ jsxRuntime.jsx(AnimatedNumber, { raw: part }, i) : part
2519
+ ) });
2520
+ }
2457
2521
  function trackClick(tracking, label, url, position) {
2458
2522
  if (!tracking?.gatewayUrl || !tracking?.code) return;
2459
2523
  const body = JSON.stringify({ label, url, position });
@@ -2536,7 +2600,7 @@ function HeroSection({ section, theme, tracking, onEvent }) {
2536
2600
  lineHeight: 1.15,
2537
2601
  letterSpacing: "-0.02em",
2538
2602
  color: theme.fg
2539
- }, children: title }),
2603
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: title }) }),
2540
2604
  subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
2541
2605
  fontSize: "0.85rem",
2542
2606
  color: theme.accent,
@@ -2711,7 +2775,7 @@ function StatsSection({ section, theme }) {
2711
2775
  letterSpacing: "0.15em",
2712
2776
  color: `${theme.fg}66`
2713
2777
  }, children: stat.label }),
2714
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: stat.value })
2778
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) })
2715
2779
  ] }),
2716
2780
  i < stats.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } })
2717
2781
  ] }, i)) });
@@ -2732,7 +2796,7 @@ function StatsSection({ section, theme }) {
2732
2796
  fontWeight: 300,
2733
2797
  lineHeight: 1,
2734
2798
  color: theme.fg
2735
- }, children: stat.value }),
2799
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) }),
2736
2800
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
2737
2801
  fontSize: 11,
2738
2802
  fontWeight: 500,
@@ -2840,6 +2904,8 @@ function LeadCaptureSection({ section, data, theme, onEvent }) {
2840
2904
  if (!email || !storeId) return;
2841
2905
  setStatus("loading");
2842
2906
  setErrorMsg("");
2907
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
2908
+ const analyticsData = data.analyticsContext;
2843
2909
  try {
2844
2910
  const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
2845
2911
  method: "POST",
@@ -2854,7 +2920,13 @@ function LeadCaptureSection({ section, data, theme, onEvent }) {
2854
2920
  const t = [...c.tags || []];
2855
2921
  if (newsletterOptIn) t.push(c.newsletter_tag || "newsletter-subscriber");
2856
2922
  return t.length > 0 ? t : void 0;
2857
- })()
2923
+ })(),
2924
+ visitor_id: analyticsData?.visitorId || void 0,
2925
+ session_id: analyticsData?.sessionId || void 0,
2926
+ utm_source: urlParams?.get("utm_source") || void 0,
2927
+ utm_medium: urlParams?.get("utm_medium") || void 0,
2928
+ utm_campaign: urlParams?.get("utm_campaign") || void 0,
2929
+ utm_content: urlParams?.get("utm_content") || void 0
2858
2930
  })
2859
2931
  });
2860
2932
  if (!res.ok) {
@@ -3400,7 +3472,8 @@ function LandingPage({
3400
3472
  renderSection,
3401
3473
  onDataLoaded,
3402
3474
  onError,
3403
- onEvent
3475
+ onEvent,
3476
+ analyticsContext
3404
3477
  }) {
3405
3478
  const [state, setState] = react.useState("loading");
3406
3479
  const [data, setData] = react.useState(null);
@@ -3448,13 +3521,14 @@ function LandingPage({
3448
3521
  if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired2, {});
3449
3522
  if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError2, { message: errorMsg });
3450
3523
  if (!data) return null;
3451
- return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent });
3524
+ return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext });
3452
3525
  }
3453
3526
  function PageLayout({
3454
3527
  data,
3455
3528
  gatewayUrl,
3456
3529
  renderSection,
3457
- onEvent
3530
+ onEvent,
3531
+ analyticsContext
3458
3532
  }) {
3459
3533
  const { landing_page: lp, store } = data;
3460
3534
  const theme = {
@@ -3469,7 +3543,7 @@ function PageLayout({
3469
3543
  const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
3470
3544
  const logoUrl = store?.logo_url;
3471
3545
  const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
3472
- const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug } };
3546
+ const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
3473
3547
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
3474
3548
  lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
3475
3549
  logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),