@neowhale/storefront 0.2.32 → 0.2.34

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  var chunkUW2U5BRY_cjs = require('../chunk-UW2U5BRY.cjs');
4
4
  var chunkHSGM4XMK_cjs = require('../chunk-HSGM4XMK.cjs');
5
+ var chunkHOQKBNYB_cjs = require('../chunk-HOQKBNYB.cjs');
5
6
  var react = require('react');
6
7
  var navigation = require('next/navigation');
7
8
  var vanilla = require('zustand/vanilla');
@@ -685,334 +686,6 @@ function PixelInitializer({ onReady, onTheme }) {
685
686
  }, [ctx, onReady, onTheme]);
686
687
  return null;
687
688
  }
688
-
689
- // src/behavioral/tracker.ts
690
- var SCROLL_MILESTONES = [25, 50, 75, 100];
691
- var TIME_MILESTONES = [30, 60, 120, 300];
692
- var MOUSE_THROTTLE_MS = 200;
693
- var MOUSE_BUFFER_MAX = 100;
694
- var RAGE_CLICK_COUNT = 3;
695
- var RAGE_CLICK_RADIUS = 50;
696
- var RAGE_CLICK_WINDOW_MS = 2e3;
697
- var MAX_CLICK_HISTORY = 10;
698
- var BehavioralTracker = class {
699
- constructor(config) {
700
- this.buffer = [];
701
- this.pageUrl = "";
702
- this.pagePath = "";
703
- this.flushTimer = null;
704
- this.scrollMilestones = /* @__PURE__ */ new Set();
705
- this.timeMilestones = /* @__PURE__ */ new Set();
706
- this.timeTimers = [];
707
- this.exitIntentFired = false;
708
- this.startTime = 0;
709
- this.clickHistory = [];
710
- this.mouseBuffer = [];
711
- this.lastMouseTime = 0;
712
- this.listeners = [];
713
- this.observer = null;
714
- this.sentinels = [];
715
- // ---------------------------------------------------------------------------
716
- // Event handlers (arrow functions for stable `this`)
717
- // ---------------------------------------------------------------------------
718
- this.handleClick = (e) => {
719
- const me = e;
720
- const target = me.target;
721
- if (!target) return;
722
- const now2 = Date.now();
723
- const x = me.clientX;
724
- const y = me.clientY;
725
- this.clickHistory.push({ x, y, t: now2 });
726
- if (this.clickHistory.length > MAX_CLICK_HISTORY) {
727
- this.clickHistory.shift();
728
- }
729
- const tag = target.tagName?.toLowerCase() ?? "";
730
- const rawText = target.textContent ?? "";
731
- const text = rawText.trim().slice(0, 50);
732
- this.push({
733
- data_type: "click",
734
- data: {
735
- tag,
736
- text,
737
- selector: this.getSelector(target),
738
- x,
739
- y,
740
- timestamp: now2
741
- },
742
- page_url: this.pageUrl,
743
- page_path: this.pagePath
744
- });
745
- this.detectRageClick(x, y, now2);
746
- };
747
- this.handleMouseMove = (e) => {
748
- const me = e;
749
- const now2 = Date.now();
750
- if (now2 - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
751
- this.lastMouseTime = now2;
752
- this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now2 });
753
- if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
754
- this.mouseBuffer.shift();
755
- }
756
- };
757
- this.handleMouseOut = (e) => {
758
- const me = e;
759
- if (this.exitIntentFired) return;
760
- if (me.clientY > 0) return;
761
- if (me.relatedTarget !== null) return;
762
- this.exitIntentFired = true;
763
- this.push({
764
- data_type: "exit_intent",
765
- data: {
766
- time_on_page_ms: Date.now() - this.startTime,
767
- timestamp: Date.now()
768
- },
769
- page_url: this.pageUrl,
770
- page_path: this.pagePath
771
- });
772
- };
773
- this.handleCopy = () => {
774
- const selection = window.getSelection();
775
- const length = selection?.toString().length ?? 0;
776
- this.push({
777
- data_type: "copy",
778
- data: {
779
- text_length: length,
780
- timestamp: Date.now()
781
- },
782
- page_url: this.pageUrl,
783
- page_path: this.pagePath
784
- });
785
- };
786
- this.handleVisibilityChange = () => {
787
- if (document.visibilityState !== "hidden") return;
788
- const timeSpent = Date.now() - this.startTime;
789
- this.push({
790
- data_type: "page_exit",
791
- data: {
792
- time_spent_ms: timeSpent,
793
- timestamp: Date.now()
794
- },
795
- page_url: this.pageUrl,
796
- page_path: this.pagePath
797
- });
798
- this.flushMouseBuffer();
799
- this.flush();
800
- };
801
- this.config = {
802
- sendBatch: config.sendBatch,
803
- sessionId: config.sessionId,
804
- visitorId: config.visitorId,
805
- flushIntervalMs: config.flushIntervalMs ?? 1e4,
806
- maxBufferSize: config.maxBufferSize ?? 500
807
- };
808
- }
809
- start() {
810
- this.startTime = Date.now();
811
- this.addListener(document, "click", this.handleClick);
812
- this.addListener(document, "mousemove", this.handleMouseMove);
813
- this.addListener(document, "mouseout", this.handleMouseOut);
814
- this.addListener(document, "copy", this.handleCopy);
815
- this.addListener(document, "visibilitychange", this.handleVisibilityChange);
816
- this.setupScrollTracking();
817
- this.setupTimeMilestones();
818
- this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
819
- }
820
- stop() {
821
- for (const [target, event, handler] of this.listeners) {
822
- target.removeEventListener(event, handler, { capture: true });
823
- }
824
- this.listeners = [];
825
- if (this.flushTimer !== null) {
826
- clearInterval(this.flushTimer);
827
- this.flushTimer = null;
828
- }
829
- this.clearTimeMilestones();
830
- this.cleanupScrollTracking();
831
- this.flushMouseBuffer();
832
- this.flush();
833
- }
834
- setPageContext(url, path) {
835
- this.flushMouseBuffer();
836
- this.flush();
837
- this.pageUrl = url;
838
- this.pagePath = path;
839
- this.scrollMilestones.clear();
840
- this.timeMilestones.clear();
841
- this.exitIntentFired = false;
842
- this.startTime = Date.now();
843
- this.clickHistory = [];
844
- this.clearTimeMilestones();
845
- this.cleanupScrollTracking();
846
- this.setupTimeMilestones();
847
- requestAnimationFrame(() => this.setupScrollTracking());
848
- }
849
- // ---------------------------------------------------------------------------
850
- // Buffer management
851
- // ---------------------------------------------------------------------------
852
- push(event) {
853
- this.buffer.push(event);
854
- if (this.buffer.length >= this.config.maxBufferSize) {
855
- this.flush();
856
- }
857
- }
858
- flush() {
859
- if (this.buffer.length === 0) return;
860
- const batch = {
861
- session_id: this.config.sessionId,
862
- visitor_id: this.config.visitorId,
863
- events: this.buffer
864
- };
865
- this.buffer = [];
866
- this.config.sendBatch(batch).catch(() => {
867
- });
868
- }
869
- addListener(target, event, handler) {
870
- target.addEventListener(event, handler, { passive: true, capture: true });
871
- this.listeners.push([target, event, handler]);
872
- }
873
- // ---------------------------------------------------------------------------
874
- // Scroll tracking with IntersectionObserver
875
- // ---------------------------------------------------------------------------
876
- setupScrollTracking() {
877
- if (typeof IntersectionObserver === "undefined") return;
878
- this.observer = new IntersectionObserver(
879
- (entries) => {
880
- for (const entry of entries) {
881
- if (!entry.isIntersecting) continue;
882
- const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
883
- if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
884
- this.scrollMilestones.add(milestone);
885
- this.push({
886
- data_type: "scroll_depth",
887
- data: {
888
- depth_percent: milestone,
889
- timestamp: Date.now()
890
- },
891
- page_url: this.pageUrl,
892
- page_path: this.pagePath
893
- });
894
- }
895
- },
896
- { threshold: 0 }
897
- );
898
- const docHeight = document.documentElement.scrollHeight;
899
- for (const pct of SCROLL_MILESTONES) {
900
- const sentinel = document.createElement("div");
901
- sentinel.setAttribute("data-scroll-milestone", String(pct));
902
- sentinel.style.position = "absolute";
903
- sentinel.style.left = "0";
904
- sentinel.style.width = "1px";
905
- sentinel.style.height = "1px";
906
- sentinel.style.pointerEvents = "none";
907
- sentinel.style.opacity = "0";
908
- sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
909
- document.body.appendChild(sentinel);
910
- this.sentinels.push(sentinel);
911
- this.observer.observe(sentinel);
912
- }
913
- }
914
- cleanupScrollTracking() {
915
- if (this.observer) {
916
- this.observer.disconnect();
917
- this.observer = null;
918
- }
919
- for (const sentinel of this.sentinels) {
920
- sentinel.remove();
921
- }
922
- this.sentinels = [];
923
- }
924
- // ---------------------------------------------------------------------------
925
- // Time milestones
926
- // ---------------------------------------------------------------------------
927
- setupTimeMilestones() {
928
- for (const seconds of TIME_MILESTONES) {
929
- const timer = setTimeout(() => {
930
- if (this.timeMilestones.has(seconds)) return;
931
- this.timeMilestones.add(seconds);
932
- this.push({
933
- data_type: "time_on_page",
934
- data: {
935
- milestone_seconds: seconds,
936
- timestamp: Date.now()
937
- },
938
- page_url: this.pageUrl,
939
- page_path: this.pagePath
940
- });
941
- }, seconds * 1e3);
942
- this.timeTimers.push(timer);
943
- }
944
- }
945
- clearTimeMilestones() {
946
- for (const timer of this.timeTimers) {
947
- clearTimeout(timer);
948
- }
949
- this.timeTimers = [];
950
- }
951
- // ---------------------------------------------------------------------------
952
- // Rage click detection
953
- // ---------------------------------------------------------------------------
954
- detectRageClick(x, y, now2) {
955
- const windowStart = now2 - RAGE_CLICK_WINDOW_MS;
956
- const nearby = this.clickHistory.filter((c) => {
957
- if (c.t < windowStart) return false;
958
- const dx = c.x - x;
959
- const dy = c.y - y;
960
- return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
961
- });
962
- if (nearby.length >= RAGE_CLICK_COUNT) {
963
- this.push({
964
- data_type: "rage_click",
965
- data: {
966
- x,
967
- y,
968
- click_count: nearby.length,
969
- timestamp: now2
970
- },
971
- page_url: this.pageUrl,
972
- page_path: this.pagePath
973
- });
974
- this.clickHistory = [];
975
- }
976
- }
977
- // ---------------------------------------------------------------------------
978
- // Mouse buffer flush
979
- // ---------------------------------------------------------------------------
980
- flushMouseBuffer() {
981
- if (this.mouseBuffer.length === 0) return;
982
- this.push({
983
- data_type: "mouse_movement",
984
- data: {
985
- points: [...this.mouseBuffer],
986
- timestamp: Date.now()
987
- },
988
- page_url: this.pageUrl,
989
- page_path: this.pagePath
990
- });
991
- this.mouseBuffer = [];
992
- }
993
- // ---------------------------------------------------------------------------
994
- // CSS selector helper
995
- // ---------------------------------------------------------------------------
996
- getSelector(el) {
997
- const parts = [];
998
- let current = el;
999
- let depth = 0;
1000
- while (current && depth < 3) {
1001
- let segment = current.tagName.toLowerCase();
1002
- if (current.id) {
1003
- segment += `#${current.id}`;
1004
- } else if (current.classList.length > 0) {
1005
- segment += `.${Array.from(current.classList).join(".")}`;
1006
- }
1007
- parts.unshift(segment);
1008
- current = current.parentElement;
1009
- depth++;
1010
- }
1011
- return parts.join(" > ");
1012
- }
1013
- };
1014
-
1015
- // src/react/components/behavioral-tracker.tsx
1016
689
  var SESSION_KEY_SUFFIX2 = "-analytics-session";
1017
690
  var VISITOR_KEY_SUFFIX2 = "-visitor-id";
1018
691
  var MAX_SESSION_WAIT_MS = 1e4;
@@ -1073,7 +746,7 @@ function BehavioralTrackerComponent({ pathname }) {
1073
746
  "x-api-key": config.apiKey
1074
747
  });
1075
748
  };
1076
- const tracker = new BehavioralTracker({ sendBatch, sessionId, visitorId });
749
+ const tracker = new chunkHOQKBNYB_cjs.BehavioralTracker({ sendBatch, sessionId, visitorId });
1077
750
  tracker.start();
1078
751
  trackerRef.current = tracker;
1079
752
  };
@@ -2572,7 +2245,26 @@ function SectionRenderer({
2572
2245
  return null;
2573
2246
  }
2574
2247
  })();
2575
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2248
+ const sectionRef = react.useRef(null);
2249
+ react.useEffect(() => {
2250
+ const el2 = sectionRef.current;
2251
+ if (!el2 || typeof IntersectionObserver === "undefined") return;
2252
+ const obs = new IntersectionObserver(
2253
+ ([entry]) => {
2254
+ if (entry.isIntersecting) {
2255
+ onEvent?.("section_view", {
2256
+ section_id: section.id,
2257
+ section_type: section.type
2258
+ });
2259
+ obs.disconnect();
2260
+ }
2261
+ },
2262
+ { threshold: 0.5 }
2263
+ );
2264
+ obs.observe(el2);
2265
+ return () => obs.disconnect();
2266
+ }, [section.id, section.type, onEvent]);
2267
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
2576
2268
  el,
2577
2269
  showCOA && data?.coa && /* @__PURE__ */ jsxRuntime.jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
2578
2270
  ] });
@@ -3479,7 +3171,8 @@ function LandingPage({
3479
3171
  onDataLoaded,
3480
3172
  onError,
3481
3173
  onEvent,
3482
- analyticsContext
3174
+ analyticsContext,
3175
+ enableAnalytics = true
3483
3176
  }) {
3484
3177
  const [state, setState] = react.useState("loading");
3485
3178
  const [data, setData] = react.useState(null);
@@ -3487,6 +3180,13 @@ function LandingPage({
3487
3180
  react.useEffect(() => {
3488
3181
  if (!slug) return;
3489
3182
  let cancelled = false;
3183
+ if (typeof window !== "undefined" && window.__LANDING_DATA__) {
3184
+ const json = window.__LANDING_DATA__;
3185
+ setData(json);
3186
+ setState("ready");
3187
+ onDataLoaded?.(json);
3188
+ return;
3189
+ }
3490
3190
  async function load() {
3491
3191
  try {
3492
3192
  const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
@@ -3527,16 +3227,113 @@ function LandingPage({
3527
3227
  if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired2, {});
3528
3228
  if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError2, { message: errorMsg });
3529
3229
  if (!data) return null;
3530
- return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext });
3230
+ return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext, enableAnalytics });
3231
+ }
3232
+ function isSectionVisible(section, urlParams) {
3233
+ const vis = section.config?.visibility;
3234
+ if (!vis?.params) return true;
3235
+ for (const [key, allowed] of Object.entries(vis.params)) {
3236
+ const val = urlParams.get(key);
3237
+ if (!val || !allowed.includes(val)) return false;
3238
+ }
3239
+ return true;
3531
3240
  }
3532
3241
  function PageLayout({
3533
3242
  data,
3534
3243
  gatewayUrl,
3535
3244
  renderSection,
3536
3245
  onEvent,
3537
- analyticsContext
3246
+ analyticsContext,
3247
+ enableAnalytics
3538
3248
  }) {
3539
3249
  const { landing_page: lp, store } = data;
3250
+ const trackerRef = react.useRef(null);
3251
+ react.useEffect(() => {
3252
+ if (!enableAnalytics || typeof window === "undefined") return;
3253
+ const analyticsConfig = window.__LANDING_ANALYTICS__;
3254
+ if (!analyticsConfig?.slug) return;
3255
+ let visitorId = localStorage.getItem("wt_vid") || "";
3256
+ if (!visitorId) {
3257
+ visitorId = crypto.randomUUID();
3258
+ localStorage.setItem("wt_vid", visitorId);
3259
+ }
3260
+ let sessionId = sessionStorage.getItem("wt_sid") || "";
3261
+ if (!sessionId) {
3262
+ sessionId = crypto.randomUUID();
3263
+ sessionStorage.setItem("wt_sid", sessionId);
3264
+ }
3265
+ import('../tracker-WYKTEQ4F.cjs').then(({ BehavioralTracker: BehavioralTracker2 }) => {
3266
+ const gwUrl = analyticsConfig.gatewayUrl || gatewayUrl;
3267
+ const slug = analyticsConfig.slug;
3268
+ const utmParams = new URLSearchParams(window.location.search);
3269
+ const tracker = new BehavioralTracker2({
3270
+ sessionId,
3271
+ visitorId,
3272
+ sendBatch: async (batch) => {
3273
+ const events = batch.events.map((e) => ({
3274
+ event_type: e.data_type,
3275
+ event_data: e.data,
3276
+ session_id: batch.session_id,
3277
+ visitor_id: batch.visitor_id,
3278
+ campaign_id: analyticsConfig.campaignId || utmParams.get("utm_campaign_id") || void 0,
3279
+ utm_source: utmParams.get("utm_source") || void 0,
3280
+ utm_medium: utmParams.get("utm_medium") || void 0,
3281
+ utm_campaign: utmParams.get("utm_campaign") || void 0
3282
+ }));
3283
+ const body = JSON.stringify({ events });
3284
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
3285
+ navigator.sendBeacon(
3286
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
3287
+ new Blob([body], { type: "application/json" })
3288
+ );
3289
+ } else {
3290
+ await fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
3291
+ method: "POST",
3292
+ headers: { "Content-Type": "application/json" },
3293
+ body,
3294
+ keepalive: true
3295
+ });
3296
+ }
3297
+ }
3298
+ });
3299
+ tracker.setPageContext(window.location.href, window.location.pathname);
3300
+ tracker.start();
3301
+ trackerRef.current = tracker;
3302
+ const pageViewBody = JSON.stringify({
3303
+ events: [{
3304
+ event_type: "page_view",
3305
+ event_data: { referrer: document.referrer, url: window.location.href },
3306
+ session_id: sessionId,
3307
+ visitor_id: visitorId,
3308
+ campaign_id: analyticsConfig.campaignId || void 0,
3309
+ utm_source: utmParams.get("utm_source") || void 0,
3310
+ utm_medium: utmParams.get("utm_medium") || void 0,
3311
+ utm_campaign: utmParams.get("utm_campaign") || void 0
3312
+ }]
3313
+ });
3314
+ if (navigator.sendBeacon) {
3315
+ navigator.sendBeacon(
3316
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
3317
+ new Blob([pageViewBody], { type: "application/json" })
3318
+ );
3319
+ } else {
3320
+ fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
3321
+ method: "POST",
3322
+ headers: { "Content-Type": "application/json" },
3323
+ body: pageViewBody,
3324
+ keepalive: true
3325
+ }).catch(() => {
3326
+ });
3327
+ }
3328
+ }).catch(() => {
3329
+ });
3330
+ return () => {
3331
+ if (trackerRef.current) {
3332
+ trackerRef.current.stop();
3333
+ trackerRef.current = null;
3334
+ }
3335
+ };
3336
+ }, [enableAnalytics, gatewayUrl]);
3540
3337
  const theme = {
3541
3338
  bg: lp.background_color || store?.theme?.background || "#050505",
3542
3339
  fg: lp.text_color || store?.theme?.foreground || "#fafafa",
@@ -3548,7 +3345,8 @@ function PageLayout({
3548
3345
  };
3549
3346
  const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
3550
3347
  const logoUrl = store?.logo_url;
3551
- const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
3348
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : new URLSearchParams();
3349
+ const sorted = [...lp.sections].sort((a, b) => a.order - b.order).filter((s) => isSectionVisible(s, urlParams));
3552
3350
  const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
3553
3351
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
3554
3352
  lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),