@neowhale/storefront 0.2.33 → 0.2.35

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.
@@ -0,0 +1,1365 @@
1
+ var WhaleStorefront = (function (exports) {
2
+ 'use strict';
3
+
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+
14
+ // src/behavioral/tracker.ts
15
+ var tracker_exports = {};
16
+ __export(tracker_exports, {
17
+ BehavioralTracker: () => exports.BehavioralTracker
18
+ });
19
+ var SCROLL_MILESTONES, TIME_MILESTONES, MOUSE_THROTTLE_MS, MOUSE_BUFFER_MAX, RAGE_CLICK_COUNT, RAGE_CLICK_RADIUS, RAGE_CLICK_WINDOW_MS, MAX_CLICK_HISTORY; exports.BehavioralTracker = void 0;
20
+ var init_tracker = __esm({
21
+ "src/behavioral/tracker.ts"() {
22
+ SCROLL_MILESTONES = [25, 50, 75, 100];
23
+ TIME_MILESTONES = [30, 60, 120, 300];
24
+ MOUSE_THROTTLE_MS = 200;
25
+ MOUSE_BUFFER_MAX = 100;
26
+ RAGE_CLICK_COUNT = 3;
27
+ RAGE_CLICK_RADIUS = 50;
28
+ RAGE_CLICK_WINDOW_MS = 2e3;
29
+ MAX_CLICK_HISTORY = 10;
30
+ exports.BehavioralTracker = class {
31
+ constructor(config) {
32
+ this.buffer = [];
33
+ this.pageUrl = "";
34
+ this.pagePath = "";
35
+ this.flushTimer = null;
36
+ this.scrollMilestones = /* @__PURE__ */ new Set();
37
+ this.timeMilestones = /* @__PURE__ */ new Set();
38
+ this.timeTimers = [];
39
+ this.exitIntentFired = false;
40
+ this.startTime = 0;
41
+ this.clickHistory = [];
42
+ this.mouseBuffer = [];
43
+ this.lastMouseTime = 0;
44
+ this.listeners = [];
45
+ this.observer = null;
46
+ this.sentinels = [];
47
+ // ---------------------------------------------------------------------------
48
+ // Event handlers (arrow functions for stable `this`)
49
+ // ---------------------------------------------------------------------------
50
+ this.handleClick = (e) => {
51
+ const me = e;
52
+ const target = me.target;
53
+ if (!target) return;
54
+ const now = Date.now();
55
+ const x = me.clientX;
56
+ const y = me.clientY;
57
+ this.clickHistory.push({ x, y, t: now });
58
+ if (this.clickHistory.length > MAX_CLICK_HISTORY) {
59
+ this.clickHistory.shift();
60
+ }
61
+ const tag = target.tagName?.toLowerCase() ?? "";
62
+ const rawText = target.textContent ?? "";
63
+ const text = rawText.trim().slice(0, 50);
64
+ this.push({
65
+ data_type: "click",
66
+ data: {
67
+ tag,
68
+ text,
69
+ selector: this.getSelector(target),
70
+ x,
71
+ y,
72
+ timestamp: now
73
+ },
74
+ page_url: this.pageUrl,
75
+ page_path: this.pagePath
76
+ });
77
+ this.detectRageClick(x, y, now);
78
+ };
79
+ this.handleMouseMove = (e) => {
80
+ const me = e;
81
+ const now = Date.now();
82
+ if (now - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
83
+ this.lastMouseTime = now;
84
+ this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now });
85
+ if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
86
+ this.mouseBuffer.shift();
87
+ }
88
+ };
89
+ this.handleMouseOut = (e) => {
90
+ const me = e;
91
+ if (this.exitIntentFired) return;
92
+ if (me.clientY > 0) return;
93
+ if (me.relatedTarget !== null) return;
94
+ this.exitIntentFired = true;
95
+ this.push({
96
+ data_type: "exit_intent",
97
+ data: {
98
+ time_on_page_ms: Date.now() - this.startTime,
99
+ timestamp: Date.now()
100
+ },
101
+ page_url: this.pageUrl,
102
+ page_path: this.pagePath
103
+ });
104
+ };
105
+ this.handleCopy = () => {
106
+ const selection = window.getSelection();
107
+ const length = selection?.toString().length ?? 0;
108
+ this.push({
109
+ data_type: "copy",
110
+ data: {
111
+ text_length: length,
112
+ timestamp: Date.now()
113
+ },
114
+ page_url: this.pageUrl,
115
+ page_path: this.pagePath
116
+ });
117
+ };
118
+ this.handleVisibilityChange = () => {
119
+ if (document.visibilityState !== "hidden") return;
120
+ const timeSpent = Date.now() - this.startTime;
121
+ this.push({
122
+ data_type: "page_exit",
123
+ data: {
124
+ time_spent_ms: timeSpent,
125
+ timestamp: Date.now()
126
+ },
127
+ page_url: this.pageUrl,
128
+ page_path: this.pagePath
129
+ });
130
+ this.flushMouseBuffer();
131
+ this.flush();
132
+ };
133
+ this.config = {
134
+ sendBatch: config.sendBatch,
135
+ sessionId: config.sessionId,
136
+ visitorId: config.visitorId,
137
+ flushIntervalMs: config.flushIntervalMs ?? 1e4,
138
+ maxBufferSize: config.maxBufferSize ?? 500
139
+ };
140
+ }
141
+ start() {
142
+ this.startTime = Date.now();
143
+ this.addListener(document, "click", this.handleClick);
144
+ this.addListener(document, "mousemove", this.handleMouseMove);
145
+ this.addListener(document, "mouseout", this.handleMouseOut);
146
+ this.addListener(document, "copy", this.handleCopy);
147
+ this.addListener(document, "visibilitychange", this.handleVisibilityChange);
148
+ this.setupScrollTracking();
149
+ this.setupTimeMilestones();
150
+ this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
151
+ }
152
+ stop() {
153
+ for (const [target, event, handler] of this.listeners) {
154
+ target.removeEventListener(event, handler, { capture: true });
155
+ }
156
+ this.listeners = [];
157
+ if (this.flushTimer !== null) {
158
+ clearInterval(this.flushTimer);
159
+ this.flushTimer = null;
160
+ }
161
+ this.clearTimeMilestones();
162
+ this.cleanupScrollTracking();
163
+ this.flushMouseBuffer();
164
+ this.flush();
165
+ }
166
+ setPageContext(url, path) {
167
+ this.flushMouseBuffer();
168
+ this.flush();
169
+ this.pageUrl = url;
170
+ this.pagePath = path;
171
+ this.scrollMilestones.clear();
172
+ this.timeMilestones.clear();
173
+ this.exitIntentFired = false;
174
+ this.startTime = Date.now();
175
+ this.clickHistory = [];
176
+ this.clearTimeMilestones();
177
+ this.cleanupScrollTracking();
178
+ this.setupTimeMilestones();
179
+ requestAnimationFrame(() => this.setupScrollTracking());
180
+ }
181
+ // ---------------------------------------------------------------------------
182
+ // Buffer management
183
+ // ---------------------------------------------------------------------------
184
+ push(event) {
185
+ this.buffer.push(event);
186
+ if (this.buffer.length >= this.config.maxBufferSize) {
187
+ this.flush();
188
+ }
189
+ }
190
+ flush() {
191
+ if (this.buffer.length === 0) return;
192
+ const batch = {
193
+ session_id: this.config.sessionId,
194
+ visitor_id: this.config.visitorId,
195
+ events: this.buffer
196
+ };
197
+ this.buffer = [];
198
+ this.config.sendBatch(batch).catch(() => {
199
+ });
200
+ }
201
+ addListener(target, event, handler) {
202
+ target.addEventListener(event, handler, { passive: true, capture: true });
203
+ this.listeners.push([target, event, handler]);
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Scroll tracking with IntersectionObserver
207
+ // ---------------------------------------------------------------------------
208
+ setupScrollTracking() {
209
+ if (typeof IntersectionObserver === "undefined") return;
210
+ this.observer = new IntersectionObserver(
211
+ (entries) => {
212
+ for (const entry of entries) {
213
+ if (!entry.isIntersecting) continue;
214
+ const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
215
+ if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
216
+ this.scrollMilestones.add(milestone);
217
+ this.push({
218
+ data_type: "scroll_depth",
219
+ data: {
220
+ depth_percent: milestone,
221
+ timestamp: Date.now()
222
+ },
223
+ page_url: this.pageUrl,
224
+ page_path: this.pagePath
225
+ });
226
+ }
227
+ },
228
+ { threshold: 0 }
229
+ );
230
+ const docHeight = document.documentElement.scrollHeight;
231
+ for (const pct of SCROLL_MILESTONES) {
232
+ const sentinel = document.createElement("div");
233
+ sentinel.setAttribute("data-scroll-milestone", String(pct));
234
+ sentinel.style.position = "absolute";
235
+ sentinel.style.left = "0";
236
+ sentinel.style.width = "1px";
237
+ sentinel.style.height = "1px";
238
+ sentinel.style.pointerEvents = "none";
239
+ sentinel.style.opacity = "0";
240
+ sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
241
+ document.body.appendChild(sentinel);
242
+ this.sentinels.push(sentinel);
243
+ this.observer.observe(sentinel);
244
+ }
245
+ }
246
+ cleanupScrollTracking() {
247
+ if (this.observer) {
248
+ this.observer.disconnect();
249
+ this.observer = null;
250
+ }
251
+ for (const sentinel of this.sentinels) {
252
+ sentinel.remove();
253
+ }
254
+ this.sentinels = [];
255
+ }
256
+ // ---------------------------------------------------------------------------
257
+ // Time milestones
258
+ // ---------------------------------------------------------------------------
259
+ setupTimeMilestones() {
260
+ for (const seconds of TIME_MILESTONES) {
261
+ const timer = setTimeout(() => {
262
+ if (this.timeMilestones.has(seconds)) return;
263
+ this.timeMilestones.add(seconds);
264
+ this.push({
265
+ data_type: "time_on_page",
266
+ data: {
267
+ milestone_seconds: seconds,
268
+ timestamp: Date.now()
269
+ },
270
+ page_url: this.pageUrl,
271
+ page_path: this.pagePath
272
+ });
273
+ }, seconds * 1e3);
274
+ this.timeTimers.push(timer);
275
+ }
276
+ }
277
+ clearTimeMilestones() {
278
+ for (const timer of this.timeTimers) {
279
+ clearTimeout(timer);
280
+ }
281
+ this.timeTimers = [];
282
+ }
283
+ // ---------------------------------------------------------------------------
284
+ // Rage click detection
285
+ // ---------------------------------------------------------------------------
286
+ detectRageClick(x, y, now) {
287
+ const windowStart = now - RAGE_CLICK_WINDOW_MS;
288
+ const nearby = this.clickHistory.filter((c) => {
289
+ if (c.t < windowStart) return false;
290
+ const dx = c.x - x;
291
+ const dy = c.y - y;
292
+ return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
293
+ });
294
+ if (nearby.length >= RAGE_CLICK_COUNT) {
295
+ this.push({
296
+ data_type: "rage_click",
297
+ data: {
298
+ x,
299
+ y,
300
+ click_count: nearby.length,
301
+ timestamp: now
302
+ },
303
+ page_url: this.pageUrl,
304
+ page_path: this.pagePath
305
+ });
306
+ this.clickHistory = [];
307
+ }
308
+ }
309
+ // ---------------------------------------------------------------------------
310
+ // Mouse buffer flush
311
+ // ---------------------------------------------------------------------------
312
+ flushMouseBuffer() {
313
+ if (this.mouseBuffer.length === 0) return;
314
+ this.push({
315
+ data_type: "mouse_movement",
316
+ data: {
317
+ points: [...this.mouseBuffer],
318
+ timestamp: Date.now()
319
+ },
320
+ page_url: this.pageUrl,
321
+ page_path: this.pagePath
322
+ });
323
+ this.mouseBuffer = [];
324
+ }
325
+ // ---------------------------------------------------------------------------
326
+ // CSS selector helper
327
+ // ---------------------------------------------------------------------------
328
+ getSelector(el) {
329
+ const parts = [];
330
+ let current = el;
331
+ let depth = 0;
332
+ while (current && depth < 3) {
333
+ let segment = current.tagName.toLowerCase();
334
+ if (current.id) {
335
+ segment += `#${current.id}`;
336
+ } else if (current.classList.length > 0) {
337
+ segment += `.${Array.from(current.classList).join(".")}`;
338
+ }
339
+ parts.unshift(segment);
340
+ current = current.parentElement;
341
+ depth++;
342
+ }
343
+ return parts.join(" > ");
344
+ }
345
+ };
346
+ }
347
+ });
348
+
349
+ // src/shims/react-global.ts
350
+ var R = globalThis.React;
351
+ var {
352
+ createElement,
353
+ createContext,
354
+ forwardRef,
355
+ memo,
356
+ lazy,
357
+ useCallback,
358
+ useContext,
359
+ useEffect,
360
+ useId,
361
+ useImperativeHandle,
362
+ useLayoutEffect,
363
+ useMemo,
364
+ useReducer,
365
+ useRef,
366
+ useState,
367
+ Fragment,
368
+ Children,
369
+ cloneElement,
370
+ isValidElement,
371
+ Suspense,
372
+ startTransition,
373
+ useTransition,
374
+ useDeferredValue,
375
+ useSyncExternalStore,
376
+ useInsertionEffect,
377
+ useDebugValue
378
+ } = R;
379
+
380
+ // src/shims/jsx-runtime-global.ts
381
+ var R2 = globalThis.React;
382
+ var jsx = R2.createElement;
383
+ var jsxs = R2.createElement;
384
+ R2.createElement;
385
+ var Fragment2 = R2.Fragment;
386
+
387
+ // src/react/components/section-renderer.tsx
388
+ var NUM_PATTERN = /(\$?[\d,]+\.?\d*[+★%]?)/g;
389
+ function easeOutQuart(t) {
390
+ return 1 - Math.pow(1 - t, 4);
391
+ }
392
+ function useCountUp(target, duration, start) {
393
+ const [value, setValue] = useState(0);
394
+ const raf = useRef(0);
395
+ useEffect(() => {
396
+ if (!start) return;
397
+ const t0 = performance.now();
398
+ function tick(now) {
399
+ const elapsed = now - t0;
400
+ const progress = Math.min(elapsed / duration, 1);
401
+ setValue(Math.round(easeOutQuart(progress) * target));
402
+ if (progress < 1) raf.current = requestAnimationFrame(tick);
403
+ }
404
+ raf.current = requestAnimationFrame(tick);
405
+ return () => cancelAnimationFrame(raf.current);
406
+ }, [target, duration, start]);
407
+ return value;
408
+ }
409
+ function AnimatedNumber({ raw }) {
410
+ const ref = useRef(null);
411
+ const [visible, setVisible] = useState(false);
412
+ useEffect(() => {
413
+ const el = ref.current;
414
+ if (!el || typeof IntersectionObserver === "undefined") {
415
+ setVisible(true);
416
+ return;
417
+ }
418
+ const obs = new IntersectionObserver(([entry]) => {
419
+ if (entry.isIntersecting) {
420
+ setVisible(true);
421
+ obs.disconnect();
422
+ }
423
+ }, { threshold: 0.3 });
424
+ obs.observe(el);
425
+ return () => obs.disconnect();
426
+ }, []);
427
+ const prefix = raw.startsWith("$") ? "$" : "";
428
+ const suffix = raw.match(/[+★%]$/)?.[0] || "";
429
+ const numeric = parseFloat(raw.replace(/[\$,+★%]/g, ""));
430
+ const hasCommas = raw.includes(",");
431
+ const decimals = raw.includes(".") ? raw.split(".")[1]?.replace(/[+★%]/g, "").length || 0 : 0;
432
+ const count = useCountUp(
433
+ decimals > 0 ? Math.round(numeric * Math.pow(10, decimals)) : numeric,
434
+ 1400,
435
+ visible
436
+ );
437
+ const display = decimals > 0 ? (count / Math.pow(10, decimals)).toFixed(decimals) : hasCommas ? count.toLocaleString() : String(count);
438
+ return /* @__PURE__ */ jsxs("span", { ref, children: [
439
+ prefix,
440
+ display,
441
+ suffix
442
+ ] });
443
+ }
444
+ function AnimatedText({ text }) {
445
+ const parts = text.split(NUM_PATTERN);
446
+ return /* @__PURE__ */ jsx(Fragment2, { children: parts.map(
447
+ (part, i) => NUM_PATTERN.test(part) ? /* @__PURE__ */ jsx(AnimatedNumber, { raw: part }, i) : part
448
+ ) });
449
+ }
450
+ function trackClick(tracking, label, url, position) {
451
+ if (!tracking?.gatewayUrl || !tracking?.code) return;
452
+ const body = JSON.stringify({ label, url, position });
453
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
454
+ navigator.sendBeacon(
455
+ `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
456
+ new Blob([body], { type: "application/json" })
457
+ );
458
+ }
459
+ }
460
+ function SectionRenderer({
461
+ section,
462
+ data,
463
+ theme,
464
+ tracking,
465
+ onEvent
466
+ }) {
467
+ const [showCOA, setShowCOA] = useState(false);
468
+ const el = (() => {
469
+ switch (section.type) {
470
+ case "hero":
471
+ return /* @__PURE__ */ jsx(HeroSection, { section, theme, tracking, onEvent });
472
+ case "text":
473
+ return /* @__PURE__ */ jsx(TextSection, { section, theme });
474
+ case "image":
475
+ return /* @__PURE__ */ jsx(ImageSection, { section, theme });
476
+ case "video":
477
+ return /* @__PURE__ */ jsx(VideoSection, { section, theme });
478
+ case "gallery":
479
+ return /* @__PURE__ */ jsx(GallerySection, { section, theme });
480
+ case "cta":
481
+ return /* @__PURE__ */ jsx(CTASection, { section, theme, tracking, onEvent });
482
+ case "stats":
483
+ return /* @__PURE__ */ jsx(StatsSection, { section, theme });
484
+ case "product_card":
485
+ return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme, tracking });
486
+ case "coa_viewer":
487
+ return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
488
+ case "social_links":
489
+ return /* @__PURE__ */ jsx(SocialLinksSection, { section, theme });
490
+ case "lead_capture":
491
+ return /* @__PURE__ */ jsx(LeadCaptureSection, { section, data, theme, onEvent });
492
+ case "divider":
493
+ return /* @__PURE__ */ jsx(DividerSection, { theme });
494
+ default:
495
+ return null;
496
+ }
497
+ })();
498
+ const sectionRef = useRef(null);
499
+ useEffect(() => {
500
+ const el2 = sectionRef.current;
501
+ if (!el2 || typeof IntersectionObserver === "undefined") return;
502
+ const obs = new IntersectionObserver(
503
+ ([entry]) => {
504
+ if (entry.isIntersecting) {
505
+ onEvent?.("section_view", {
506
+ section_id: section.id,
507
+ section_type: section.type
508
+ });
509
+ obs.disconnect();
510
+ }
511
+ },
512
+ { threshold: 0.5 }
513
+ );
514
+ obs.observe(el2);
515
+ return () => obs.disconnect();
516
+ }, [section.id, section.type, onEvent]);
517
+ return /* @__PURE__ */ jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
518
+ el,
519
+ showCOA && data?.coa && /* @__PURE__ */ jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
520
+ ] });
521
+ }
522
+ function HeroSection({ section, theme, tracking, onEvent }) {
523
+ const { title, subtitle, background_image, cta_text, cta_url } = section.content;
524
+ return /* @__PURE__ */ jsxs(
525
+ "div",
526
+ {
527
+ style: {
528
+ position: "relative",
529
+ minHeight: "60vh",
530
+ display: "flex",
531
+ flexDirection: "column",
532
+ justifyContent: "center",
533
+ alignItems: "center",
534
+ textAlign: "center",
535
+ padding: "3rem 1.5rem",
536
+ backgroundImage: background_image ? `url(${background_image})` : void 0,
537
+ backgroundSize: "cover",
538
+ backgroundPosition: "center"
539
+ },
540
+ children: [
541
+ background_image && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" } }),
542
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", zIndex: 1, maxWidth: 640 }, children: [
543
+ title && /* @__PURE__ */ jsx("h1", { style: {
544
+ fontSize: "clamp(2rem, 8vw, 3rem)",
545
+ fontWeight: 300,
546
+ fontFamily: theme.fontDisplay || "inherit",
547
+ margin: "0 0 1rem",
548
+ lineHeight: 1.15,
549
+ letterSpacing: "-0.02em",
550
+ color: theme.fg
551
+ }, children: /* @__PURE__ */ jsx(AnimatedText, { text: title }) }),
552
+ subtitle && /* @__PURE__ */ jsx("p", { style: {
553
+ fontSize: "0.85rem",
554
+ color: theme.accent,
555
+ margin: "0 0 2rem",
556
+ lineHeight: 1.6,
557
+ textTransform: "uppercase",
558
+ letterSpacing: "0.15em"
559
+ }, children: subtitle }),
560
+ cta_text && cta_url && /* @__PURE__ */ jsx(
561
+ "a",
562
+ {
563
+ href: cta_url,
564
+ onClick: () => {
565
+ trackClick(tracking, cta_text, cta_url);
566
+ onEvent?.("cta_click", { label: cta_text, url: cta_url });
567
+ },
568
+ style: {
569
+ display: "inline-block",
570
+ padding: "0.875rem 2rem",
571
+ background: theme.fg,
572
+ color: theme.bg,
573
+ textDecoration: "none",
574
+ fontSize: "0.85rem",
575
+ fontWeight: 500,
576
+ letterSpacing: "0.08em",
577
+ textTransform: "uppercase"
578
+ },
579
+ children: cta_text
580
+ }
581
+ )
582
+ ] })
583
+ ]
584
+ }
585
+ );
586
+ }
587
+ function TextSection({ section, theme }) {
588
+ const { heading, body } = section.content;
589
+ const align = section.config?.align || "left";
590
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 640, margin: "0 auto", textAlign: align }, children: [
591
+ heading && /* @__PURE__ */ jsx("h2", { style: {
592
+ fontSize: 11,
593
+ fontWeight: 500,
594
+ textTransform: "uppercase",
595
+ letterSpacing: "0.25em",
596
+ color: `${theme.fg}40`,
597
+ margin: "0 0 0.75rem"
598
+ }, children: heading }),
599
+ body && /* @__PURE__ */ jsx("div", { style: { color: `${theme.fg}99`, lineHeight: 1.7, fontSize: "0.9rem", fontWeight: 300, whiteSpace: "pre-wrap" }, children: body })
600
+ ] });
601
+ }
602
+ function ImageSection({ section, theme }) {
603
+ const { url, alt, caption } = section.content;
604
+ const contained = section.config?.contained !== false;
605
+ if (!url) return null;
606
+ return /* @__PURE__ */ jsxs("div", { style: { padding: contained ? "1.5rem" : 0, maxWidth: contained ? 640 : void 0, margin: contained ? "0 auto" : void 0 }, children: [
607
+ /* @__PURE__ */ jsx(
608
+ "img",
609
+ {
610
+ src: url,
611
+ alt: alt || "",
612
+ style: { width: "100%", display: "block", objectFit: "cover" }
613
+ }
614
+ ),
615
+ caption && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: theme.muted, textAlign: "center", marginTop: "0.75rem" }, children: caption })
616
+ ] });
617
+ }
618
+ function VideoSection({ section, theme }) {
619
+ const { url, poster } = section.content;
620
+ if (!url) return null;
621
+ const isEmbed = url.includes("youtube") || url.includes("youtu.be") || url.includes("vimeo");
622
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: isEmbed ? /* @__PURE__ */ jsx("div", { style: { position: "relative", paddingBottom: "56.25%", height: 0 }, children: /* @__PURE__ */ jsx(
623
+ "iframe",
624
+ {
625
+ src: toEmbedUrl(url),
626
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: "none" },
627
+ allow: "autoplay; fullscreen",
628
+ title: "Video"
629
+ }
630
+ ) }) : /* @__PURE__ */ jsx(
631
+ "video",
632
+ {
633
+ src: url,
634
+ poster,
635
+ controls: true,
636
+ style: { width: "100%", display: "block", background: theme.surface }
637
+ }
638
+ ) });
639
+ }
640
+ function GallerySection({ section, theme }) {
641
+ const { images } = section.content;
642
+ const columns = section.config?.columns || 3;
643
+ if (!images || images.length === 0) return null;
644
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 800, margin: "0 auto" }, children: /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: "0.5rem" }, children: images.map((img, i) => /* @__PURE__ */ jsx("div", { style: { aspectRatio: "1", overflow: "hidden", background: theme.surface }, children: /* @__PURE__ */ jsx(
645
+ "img",
646
+ {
647
+ src: img.url,
648
+ alt: img.alt || "",
649
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
650
+ }
651
+ ) }, i)) }) });
652
+ }
653
+ function CTASection({ section, theme, tracking, onEvent }) {
654
+ const { title, subtitle, buttons } = section.content;
655
+ if (!buttons || buttons.length === 0) return null;
656
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
657
+ title && /* @__PURE__ */ jsx("h2", { style: {
658
+ fontSize: "clamp(1.25rem, 4vw, 1.5rem)",
659
+ fontWeight: 300,
660
+ fontFamily: theme.fontDisplay || "inherit",
661
+ margin: "0 0 0.25rem",
662
+ lineHeight: 1.2,
663
+ letterSpacing: "-0.02em",
664
+ color: theme.fg,
665
+ textAlign: "center"
666
+ }, children: title }),
667
+ subtitle && /* @__PURE__ */ jsx("p", { style: {
668
+ fontSize: "0.8rem",
669
+ color: theme.accent,
670
+ margin: "0 0 0.75rem",
671
+ lineHeight: 1.6,
672
+ textTransform: "uppercase",
673
+ letterSpacing: "0.15em",
674
+ textAlign: "center"
675
+ }, children: subtitle }),
676
+ buttons.map((btn, i) => {
677
+ const isPrimary = btn.style !== "outline";
678
+ return /* @__PURE__ */ jsx(
679
+ "a",
680
+ {
681
+ href: btn.url,
682
+ onClick: () => {
683
+ trackClick(tracking, btn.text, btn.url, i);
684
+ onEvent?.("cta_click", { label: btn.text, url: btn.url });
685
+ },
686
+ style: {
687
+ display: "block",
688
+ width: "100%",
689
+ padding: "0.875rem",
690
+ background: isPrimary ? theme.fg : "transparent",
691
+ color: isPrimary ? theme.bg : theme.fg,
692
+ border: isPrimary ? "none" : `1px solid ${theme.fg}20`,
693
+ fontSize: "0.85rem",
694
+ fontWeight: 500,
695
+ textAlign: "center",
696
+ textDecoration: "none",
697
+ boxSizing: "border-box",
698
+ letterSpacing: "0.08em",
699
+ textTransform: "uppercase"
700
+ },
701
+ children: btn.text
702
+ },
703
+ i
704
+ );
705
+ })
706
+ ] });
707
+ }
708
+ function StatsSection({ section, theme }) {
709
+ const { stats } = section.content;
710
+ const layout = section.config?.layout;
711
+ if (!stats || stats.length === 0) return null;
712
+ if (layout === "list") {
713
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: stats.map((stat, i) => /* @__PURE__ */ jsxs("div", { children: [
714
+ /* @__PURE__ */ jsxs("div", { style: {
715
+ display: "flex",
716
+ justifyContent: "space-between",
717
+ alignItems: "baseline",
718
+ padding: "0.625rem 0"
719
+ }, children: [
720
+ /* @__PURE__ */ jsx("span", { style: {
721
+ fontSize: 12,
722
+ textTransform: "uppercase",
723
+ letterSpacing: "0.15em",
724
+ color: `${theme.fg}66`
725
+ }, children: stat.label }),
726
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: /* @__PURE__ */ jsx(AnimatedText, { text: stat.value }) })
727
+ ] }),
728
+ i < stats.length - 1 && /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } })
729
+ ] }, i)) });
730
+ }
731
+ const columns = Math.min(stats.length, 4);
732
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsx("div", { style: {
733
+ display: "grid",
734
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
735
+ border: `1px solid ${theme.fg}0F`
736
+ }, children: stats.map((stat, i) => /* @__PURE__ */ jsxs("div", { style: {
737
+ padding: "1.25rem 0.5rem",
738
+ textAlign: "center",
739
+ borderRight: i < stats.length - 1 ? `1px solid ${theme.fg}0F` : void 0
740
+ }, children: [
741
+ /* @__PURE__ */ jsx("div", { style: {
742
+ fontFamily: theme.fontDisplay || "inherit",
743
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
744
+ fontWeight: 300,
745
+ lineHeight: 1,
746
+ color: theme.fg
747
+ }, children: /* @__PURE__ */ jsx(AnimatedText, { text: stat.value }) }),
748
+ /* @__PURE__ */ jsx("div", { style: {
749
+ fontSize: 11,
750
+ fontWeight: 500,
751
+ textTransform: "uppercase",
752
+ letterSpacing: "0.25em",
753
+ color: theme.accent,
754
+ marginTop: "0.5rem"
755
+ }, children: stat.label })
756
+ ] }, i)) }) });
757
+ }
758
+ function ProductCardSection({ section, data, theme, tracking }) {
759
+ const product = data?.product;
760
+ const c = section.content;
761
+ const name = c.name || product?.name || "";
762
+ const description = c.description || product?.description || "";
763
+ const imageUrl = c.image_url || product?.featured_image || null;
764
+ const url = c.url || null;
765
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxs("div", { style: { background: theme.surface, overflow: "hidden" }, children: [
766
+ imageUrl && /* @__PURE__ */ jsx("div", { style: { width: "100%", aspectRatio: "1", overflow: "hidden" }, children: /* @__PURE__ */ jsx("img", { src: imageUrl, alt: name, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }),
767
+ /* @__PURE__ */ jsxs("div", { style: { padding: "1.25rem" }, children: [
768
+ /* @__PURE__ */ jsx("h3", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem", color: theme.fg }, children: name }),
769
+ description && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.9rem", color: theme.muted, margin: "0 0 1rem", lineHeight: 1.5 }, children: description }),
770
+ url && /* @__PURE__ */ jsx(
771
+ "a",
772
+ {
773
+ href: url,
774
+ onClick: () => trackClick(tracking, "View Product", url),
775
+ style: {
776
+ display: "block",
777
+ width: "100%",
778
+ padding: "0.75rem",
779
+ background: theme.fg,
780
+ color: theme.bg,
781
+ textAlign: "center",
782
+ textDecoration: "none",
783
+ fontSize: "0.85rem",
784
+ fontWeight: 500,
785
+ boxSizing: "border-box",
786
+ letterSpacing: "0.08em",
787
+ textTransform: "uppercase"
788
+ },
789
+ children: "View Product"
790
+ }
791
+ )
792
+ ] })
793
+ ] }) });
794
+ }
795
+ function COAViewerSection({
796
+ section,
797
+ data,
798
+ theme,
799
+ onShowCOA,
800
+ tracking
801
+ }) {
802
+ const coa = data?.coa;
803
+ const c = section.content;
804
+ if (!coa) return null;
805
+ const buttonStyle = {
806
+ width: "100%",
807
+ padding: "0.875rem",
808
+ background: theme.accent,
809
+ color: theme.bg,
810
+ border: "none",
811
+ fontSize: "0.85rem",
812
+ fontWeight: 500,
813
+ cursor: "pointer",
814
+ letterSpacing: "0.08em",
815
+ textTransform: "uppercase",
816
+ textAlign: "center",
817
+ textDecoration: "none",
818
+ display: "block",
819
+ boxSizing: "border-box"
820
+ };
821
+ const buttonLabel = c.button_text || "View Lab Results";
822
+ if (coa.viewer_url) {
823
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx(
824
+ "a",
825
+ {
826
+ href: coa.viewer_url,
827
+ target: "_blank",
828
+ rel: "noopener noreferrer",
829
+ onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
830
+ style: buttonStyle,
831
+ children: buttonLabel
832
+ }
833
+ ) });
834
+ }
835
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick: () => {
836
+ trackClick(tracking, buttonLabel, coa.url);
837
+ onShowCOA();
838
+ }, style: buttonStyle, children: buttonLabel }) });
839
+ }
840
+ function LeadCaptureSection({ section, data, theme, onEvent }) {
841
+ const c = section.content;
842
+ const [firstName, setFirstName] = useState("");
843
+ const [email, setEmail] = useState("");
844
+ const [newsletterOptIn, setNewsletterOptIn] = useState(false);
845
+ const [status, setStatus] = useState("idle");
846
+ const [errorMsg, setErrorMsg] = useState("");
847
+ const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
848
+ const storeId = c.store_id || data.store?.id;
849
+ const slug = c.landing_page_slug || data.landing_page?.slug;
850
+ async function handleSubmit(e) {
851
+ e.preventDefault();
852
+ if (!email || !storeId) return;
853
+ setStatus("loading");
854
+ setErrorMsg("");
855
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
856
+ const analyticsData = data.analyticsContext;
857
+ try {
858
+ const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
859
+ method: "POST",
860
+ headers: { "Content-Type": "application/json" },
861
+ body: JSON.stringify({
862
+ email,
863
+ first_name: firstName || void 0,
864
+ source: c.source || "landing_page",
865
+ landing_page_slug: slug || void 0,
866
+ newsletter_opt_in: newsletterOptIn || void 0,
867
+ tags: (() => {
868
+ const t = [...c.tags || []];
869
+ if (newsletterOptIn) t.push(c.newsletter_tag || "newsletter-subscriber");
870
+ return t.length > 0 ? t : void 0;
871
+ })(),
872
+ visitor_id: analyticsData?.visitorId || void 0,
873
+ session_id: analyticsData?.sessionId || void 0,
874
+ utm_source: urlParams?.get("utm_source") || void 0,
875
+ utm_medium: urlParams?.get("utm_medium") || void 0,
876
+ utm_campaign: urlParams?.get("utm_campaign") || void 0,
877
+ utm_content: urlParams?.get("utm_content") || void 0
878
+ })
879
+ });
880
+ if (!res.ok) {
881
+ const body = await res.json().catch(() => ({}));
882
+ throw new Error(body?.error?.message || "Something went wrong. Please try again.");
883
+ }
884
+ setStatus("success");
885
+ onEvent?.("lead", { email, first_name: firstName || void 0, source: c.source || "landing_page", landing_page_slug: slug || void 0 });
886
+ } catch (err) {
887
+ setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
888
+ setStatus("error");
889
+ }
890
+ }
891
+ const heading = c.heading || "get 10% off your first visit.";
892
+ const subtitle = c.subtitle || "drop your email and we will send you the code.";
893
+ const buttonText = c.button_text || "Claim My Discount";
894
+ const successHeading = c.success_heading || "You\u2019re in!";
895
+ const successMessage = c.success_message || "Check your inbox for the discount code.";
896
+ const inputStyle = {
897
+ flex: 1,
898
+ minWidth: 0,
899
+ padding: "0.875rem 1rem",
900
+ background: theme.surface,
901
+ border: `1px solid ${theme.fg}15`,
902
+ color: theme.fg,
903
+ fontSize: "0.95rem",
904
+ fontWeight: 300,
905
+ outline: "none",
906
+ boxSizing: "border-box",
907
+ fontFamily: "inherit",
908
+ transition: "border-color 0.2s"
909
+ };
910
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
911
+ /* @__PURE__ */ jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
912
+ /* @__PURE__ */ jsx("div", { style: {
913
+ background: theme.surface,
914
+ border: `1px solid ${theme.fg}12`,
915
+ padding: "clamp(2rem, 6vw, 3rem)"
916
+ }, children: status === "success" ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
917
+ /* @__PURE__ */ jsx("h2", { style: {
918
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
919
+ fontWeight: 300,
920
+ fontFamily: theme.fontDisplay || "inherit",
921
+ margin: "0 0 0.75rem",
922
+ lineHeight: 1.2,
923
+ letterSpacing: "-0.02em",
924
+ color: theme.fg
925
+ }, children: successHeading }),
926
+ /* @__PURE__ */ jsx("p", { style: {
927
+ fontSize: "0.9rem",
928
+ color: `${theme.fg}99`,
929
+ margin: "0 0 1.5rem",
930
+ lineHeight: 1.6,
931
+ fontWeight: 300
932
+ }, children: successMessage }),
933
+ c.coupon_code && /* @__PURE__ */ jsx("div", { style: {
934
+ display: "inline-block",
935
+ padding: "0.75rem 2rem",
936
+ background: `${theme.fg}08`,
937
+ border: `1px dashed ${theme.fg}30`,
938
+ fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
939
+ fontWeight: 500,
940
+ fontFamily: "monospace",
941
+ letterSpacing: "0.12em",
942
+ color: theme.accent
943
+ }, children: c.coupon_code })
944
+ ] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
945
+ /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
946
+ /* @__PURE__ */ jsx("h2", { style: {
947
+ fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
948
+ fontWeight: 300,
949
+ fontFamily: theme.fontDisplay || "inherit",
950
+ margin: "0 0 0.5rem",
951
+ lineHeight: 1.15,
952
+ letterSpacing: "-0.02em",
953
+ color: theme.fg
954
+ }, children: heading }),
955
+ /* @__PURE__ */ jsx("p", { style: {
956
+ fontSize: "0.85rem",
957
+ color: theme.accent,
958
+ margin: 0,
959
+ lineHeight: 1.6,
960
+ textTransform: "uppercase",
961
+ letterSpacing: "0.15em"
962
+ }, children: subtitle })
963
+ ] }),
964
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
965
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
966
+ /* @__PURE__ */ jsx(
967
+ "input",
968
+ {
969
+ type: "text",
970
+ placeholder: "First name",
971
+ value: firstName,
972
+ onChange: (e) => setFirstName(e.target.value),
973
+ style: inputStyle
974
+ }
975
+ ),
976
+ /* @__PURE__ */ jsx(
977
+ "input",
978
+ {
979
+ type: "email",
980
+ placeholder: "Email address",
981
+ value: email,
982
+ onChange: (e) => setEmail(e.target.value),
983
+ required: true,
984
+ style: inputStyle
985
+ }
986
+ )
987
+ ] }),
988
+ c.show_newsletter_opt_in !== false && /* @__PURE__ */ jsxs("label", { style: {
989
+ display: "flex",
990
+ alignItems: "center",
991
+ gap: "0.5rem",
992
+ cursor: "pointer",
993
+ fontSize: "0.8rem",
994
+ color: `${theme.fg}90`,
995
+ fontWeight: 300,
996
+ lineHeight: 1.4
997
+ }, children: [
998
+ /* @__PURE__ */ jsx(
999
+ "input",
1000
+ {
1001
+ type: "checkbox",
1002
+ checked: newsletterOptIn,
1003
+ onChange: (e) => setNewsletterOptIn(e.target.checked),
1004
+ style: {
1005
+ width: 16,
1006
+ height: 16,
1007
+ accentColor: theme.accent,
1008
+ cursor: "pointer",
1009
+ flexShrink: 0
1010
+ }
1011
+ }
1012
+ ),
1013
+ c.newsletter_label || "Also sign me up for the newsletter \u2014 new drops, deals, and company news."
1014
+ ] }),
1015
+ status === "error" && errorMsg && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
1016
+ /* @__PURE__ */ jsxs(
1017
+ "button",
1018
+ {
1019
+ type: "submit",
1020
+ disabled: status === "loading",
1021
+ style: {
1022
+ width: "100%",
1023
+ padding: "0.875rem",
1024
+ background: theme.fg,
1025
+ color: theme.bg,
1026
+ border: "none",
1027
+ fontSize: "0.85rem",
1028
+ fontWeight: 500,
1029
+ cursor: status === "loading" ? "wait" : "pointer",
1030
+ letterSpacing: "0.08em",
1031
+ textTransform: "uppercase",
1032
+ fontFamily: "inherit",
1033
+ display: "flex",
1034
+ alignItems: "center",
1035
+ justifyContent: "center",
1036
+ gap: "0.5rem",
1037
+ opacity: status === "loading" ? 0.7 : 1,
1038
+ transition: "opacity 0.2s"
1039
+ },
1040
+ children: [
1041
+ status === "loading" && /* @__PURE__ */ jsx("span", { style: {
1042
+ display: "inline-block",
1043
+ width: 16,
1044
+ height: 16,
1045
+ border: `2px solid ${theme.bg}40`,
1046
+ borderTopColor: theme.bg,
1047
+ borderRadius: "50%",
1048
+ animation: "lc-spin 0.8s linear infinite"
1049
+ } }),
1050
+ buttonText
1051
+ ]
1052
+ }
1053
+ )
1054
+ ] })
1055
+ ] }) })
1056
+ ] });
1057
+ }
1058
+ function SocialLinksSection({ section, theme }) {
1059
+ const { links } = section.content;
1060
+ if (!links || links.length === 0) return null;
1061
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center", gap: "1.5rem", flexWrap: "wrap" }, children: links.map((link, i) => /* @__PURE__ */ jsx(
1062
+ "a",
1063
+ {
1064
+ href: link.url,
1065
+ target: "_blank",
1066
+ rel: "noopener noreferrer",
1067
+ style: {
1068
+ color: theme.muted,
1069
+ textDecoration: "none",
1070
+ fontSize: "0.85rem",
1071
+ fontWeight: 500,
1072
+ textTransform: "capitalize",
1073
+ letterSpacing: "0.03em"
1074
+ },
1075
+ children: link.platform
1076
+ },
1077
+ i
1078
+ )) });
1079
+ }
1080
+ function DividerSection({ theme }) {
1081
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1rem 1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } }) });
1082
+ }
1083
+ function COAModal({ coa, theme, onClose }) {
1084
+ return /* @__PURE__ */ jsxs("div", { style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.95)", display: "flex", flexDirection: "column" }, children: [
1085
+ /* @__PURE__ */ jsxs("div", { style: {
1086
+ display: "flex",
1087
+ justifyContent: "space-between",
1088
+ alignItems: "center",
1089
+ padding: "0.75rem 1rem",
1090
+ borderBottom: `1px solid ${theme.fg}10`
1091
+ }, children: [
1092
+ /* @__PURE__ */ jsx("span", { style: { color: "#fff", fontWeight: 500, fontSize: "0.85rem" }, children: coa.document_name || "Lab Results" }),
1093
+ /* @__PURE__ */ jsx(
1094
+ "button",
1095
+ {
1096
+ onClick: onClose,
1097
+ style: {
1098
+ background: `${theme.fg}10`,
1099
+ border: "none",
1100
+ color: "#fff",
1101
+ fontSize: "0.85rem",
1102
+ cursor: "pointer",
1103
+ padding: "0.375rem 0.75rem"
1104
+ },
1105
+ children: "Close"
1106
+ }
1107
+ )
1108
+ ] }),
1109
+ /* @__PURE__ */ jsx("iframe", { src: coa.url, style: { flex: 1, border: "none", background: "#fff" }, title: "Lab Results" })
1110
+ ] });
1111
+ }
1112
+ function toEmbedUrl(url) {
1113
+ const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
1114
+ if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
1115
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
1116
+ if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
1117
+ return url;
1118
+ }
1119
+
1120
+ // src/react/components/landing-page.tsx
1121
+ function LandingPage({
1122
+ slug,
1123
+ gatewayUrl = "https://whale-gateway.fly.dev",
1124
+ renderSection,
1125
+ onDataLoaded,
1126
+ onError,
1127
+ onEvent,
1128
+ analyticsContext,
1129
+ enableAnalytics = true
1130
+ }) {
1131
+ const [state, setState] = useState("loading");
1132
+ const [data, setData] = useState(null);
1133
+ const [errorMsg, setErrorMsg] = useState("");
1134
+ useEffect(() => {
1135
+ if (!slug) return;
1136
+ let cancelled = false;
1137
+ if (typeof window !== "undefined" && window.__LANDING_DATA__) {
1138
+ const json = window.__LANDING_DATA__;
1139
+ setData(json);
1140
+ setState("ready");
1141
+ onDataLoaded?.(json);
1142
+ return;
1143
+ }
1144
+ async function load() {
1145
+ try {
1146
+ const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
1147
+ if (!cancelled) {
1148
+ if (res.status === 404) {
1149
+ setState("not_found");
1150
+ return;
1151
+ }
1152
+ if (res.status === 410) {
1153
+ setState("expired");
1154
+ return;
1155
+ }
1156
+ if (!res.ok) {
1157
+ const body = await res.json().catch(() => ({}));
1158
+ throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1159
+ }
1160
+ const json = await res.json();
1161
+ setData(json);
1162
+ setState("ready");
1163
+ onDataLoaded?.(json);
1164
+ }
1165
+ } catch (err) {
1166
+ if (!cancelled) {
1167
+ const e = err instanceof Error ? err : new Error(String(err));
1168
+ setErrorMsg(e.message);
1169
+ setState("error");
1170
+ onError?.(e);
1171
+ }
1172
+ }
1173
+ }
1174
+ load();
1175
+ return () => {
1176
+ cancelled = true;
1177
+ };
1178
+ }, [slug, gatewayUrl]);
1179
+ if (state === "loading") return /* @__PURE__ */ jsx(DefaultLoading, {});
1180
+ if (state === "not_found") return /* @__PURE__ */ jsx(DefaultNotFound, {});
1181
+ if (state === "expired") return /* @__PURE__ */ jsx(DefaultExpired, {});
1182
+ if (state === "error") return /* @__PURE__ */ jsx(DefaultError, { message: errorMsg });
1183
+ if (!data) return null;
1184
+ return /* @__PURE__ */ jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext, enableAnalytics });
1185
+ }
1186
+ function isSectionVisible(section, urlParams) {
1187
+ const vis = section.config?.visibility;
1188
+ if (!vis?.params) return true;
1189
+ for (const [key, allowed] of Object.entries(vis.params)) {
1190
+ const val = urlParams.get(key);
1191
+ if (!val || !allowed.includes(val)) return false;
1192
+ }
1193
+ return true;
1194
+ }
1195
+ function PageLayout({
1196
+ data,
1197
+ gatewayUrl,
1198
+ renderSection,
1199
+ onEvent,
1200
+ analyticsContext,
1201
+ enableAnalytics
1202
+ }) {
1203
+ const { landing_page: lp, store } = data;
1204
+ const trackerRef = useRef(null);
1205
+ useEffect(() => {
1206
+ if (!enableAnalytics || typeof window === "undefined") return;
1207
+ const analyticsConfig = window.__LANDING_ANALYTICS__;
1208
+ if (!analyticsConfig?.slug) return;
1209
+ let visitorId = localStorage.getItem("wt_vid") || "";
1210
+ if (!visitorId) {
1211
+ visitorId = crypto.randomUUID();
1212
+ localStorage.setItem("wt_vid", visitorId);
1213
+ }
1214
+ let sessionId = sessionStorage.getItem("wt_sid") || "";
1215
+ if (!sessionId) {
1216
+ sessionId = crypto.randomUUID();
1217
+ sessionStorage.setItem("wt_sid", sessionId);
1218
+ }
1219
+ Promise.resolve().then(() => (init_tracker(), tracker_exports)).then(({ BehavioralTracker: BehavioralTracker2 }) => {
1220
+ const gwUrl = analyticsConfig.gatewayUrl || gatewayUrl;
1221
+ const slug = analyticsConfig.slug;
1222
+ const utmParams = new URLSearchParams(window.location.search);
1223
+ const tracker = new BehavioralTracker2({
1224
+ sessionId,
1225
+ visitorId,
1226
+ sendBatch: async (batch) => {
1227
+ const events = batch.events.map((e) => ({
1228
+ event_type: e.data_type,
1229
+ event_data: e.data,
1230
+ session_id: batch.session_id,
1231
+ visitor_id: batch.visitor_id,
1232
+ campaign_id: analyticsConfig.campaignId || utmParams.get("utm_campaign_id") || void 0,
1233
+ utm_source: utmParams.get("utm_source") || void 0,
1234
+ utm_medium: utmParams.get("utm_medium") || void 0,
1235
+ utm_campaign: utmParams.get("utm_campaign") || void 0
1236
+ }));
1237
+ const body = JSON.stringify({ events });
1238
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1239
+ navigator.sendBeacon(
1240
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1241
+ new Blob([body], { type: "application/json" })
1242
+ );
1243
+ } else {
1244
+ await fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1245
+ method: "POST",
1246
+ headers: { "Content-Type": "application/json" },
1247
+ body,
1248
+ keepalive: true
1249
+ });
1250
+ }
1251
+ }
1252
+ });
1253
+ tracker.setPageContext(window.location.href, window.location.pathname);
1254
+ tracker.start();
1255
+ trackerRef.current = tracker;
1256
+ const pageViewBody = JSON.stringify({
1257
+ events: [{
1258
+ event_type: "page_view",
1259
+ event_data: { referrer: document.referrer, url: window.location.href },
1260
+ session_id: sessionId,
1261
+ visitor_id: visitorId,
1262
+ campaign_id: analyticsConfig.campaignId || void 0,
1263
+ utm_source: utmParams.get("utm_source") || void 0,
1264
+ utm_medium: utmParams.get("utm_medium") || void 0,
1265
+ utm_campaign: utmParams.get("utm_campaign") || void 0
1266
+ }]
1267
+ });
1268
+ if (navigator.sendBeacon) {
1269
+ navigator.sendBeacon(
1270
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1271
+ new Blob([pageViewBody], { type: "application/json" })
1272
+ );
1273
+ } else {
1274
+ fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1275
+ method: "POST",
1276
+ headers: { "Content-Type": "application/json" },
1277
+ body: pageViewBody,
1278
+ keepalive: true
1279
+ }).catch(() => {
1280
+ });
1281
+ }
1282
+ }).catch(() => {
1283
+ });
1284
+ return () => {
1285
+ if (trackerRef.current) {
1286
+ trackerRef.current.stop();
1287
+ trackerRef.current = null;
1288
+ }
1289
+ };
1290
+ }, [enableAnalytics, gatewayUrl]);
1291
+ const theme = {
1292
+ bg: lp.background_color || store?.theme?.background || "#050505",
1293
+ fg: lp.text_color || store?.theme?.foreground || "#fafafa",
1294
+ accent: lp.accent_color || store?.theme?.accent || "#E8E2D9",
1295
+ surface: store?.theme?.surface || "#111",
1296
+ muted: store?.theme?.muted || "#888",
1297
+ fontDisplay: store?.theme?.fontDisplay || void 0,
1298
+ fontBody: store?.theme?.fontBody || void 0
1299
+ };
1300
+ const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
1301
+ const logoUrl = store?.logo_url;
1302
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : new URLSearchParams();
1303
+ const sorted = [...lp.sections].sort((a, b) => a.order - b.order).filter((s) => isSectionVisible(s, urlParams));
1304
+ const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
1305
+ return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1306
+ lp.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
1307
+ logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
1308
+ sorted.map((section) => {
1309
+ const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1310
+ if (renderSection) {
1311
+ return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1312
+ }
1313
+ return /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1314
+ }),
1315
+ store?.name && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1316
+ "Powered by ",
1317
+ store.name
1318
+ ] }) })
1319
+ ] });
1320
+ }
1321
+ var containerStyle = {
1322
+ minHeight: "100dvh",
1323
+ display: "flex",
1324
+ justifyContent: "center",
1325
+ alignItems: "center",
1326
+ fontFamily: "system-ui, -apple-system, sans-serif",
1327
+ background: "#050505",
1328
+ color: "#fafafa",
1329
+ textAlign: "center",
1330
+ padding: "2rem"
1331
+ };
1332
+ function DefaultLoading() {
1333
+ return /* @__PURE__ */ jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxs("div", { children: [
1334
+ /* @__PURE__ */ jsx("div", { style: { width: 32, height: 32, border: "2px solid #333", borderTopColor: "#fafafa", borderRadius: "50%", animation: "spin 0.8s linear infinite", margin: "0 auto 1rem" } }),
1335
+ /* @__PURE__ */ jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1336
+ ] }) });
1337
+ }
1338
+ function DefaultNotFound() {
1339
+ return /* @__PURE__ */ jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxs("div", { children: [
1340
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Not Found" }),
1341
+ /* @__PURE__ */ jsx("p", { style: { color: "#888" }, children: "This page does not exist or has been removed." })
1342
+ ] }) });
1343
+ }
1344
+ function DefaultExpired() {
1345
+ return /* @__PURE__ */ jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxs("div", { children: [
1346
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Expired" }),
1347
+ /* @__PURE__ */ jsx("p", { style: { color: "#888" }, children: "This page is no longer active." })
1348
+ ] }) });
1349
+ }
1350
+ function DefaultError({ message }) {
1351
+ return /* @__PURE__ */ jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxs("div", { children: [
1352
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Something Went Wrong" }),
1353
+ /* @__PURE__ */ jsx("p", { style: { color: "#888" }, children: message || "Please try again later." })
1354
+ ] }) });
1355
+ }
1356
+
1357
+ // src/landing-entry.ts
1358
+ init_tracker();
1359
+
1360
+ exports.LandingPage = LandingPage;
1361
+ exports.SectionRenderer = SectionRenderer;
1362
+
1363
+ return exports;
1364
+
1365
+ })({});