@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.
@@ -0,0 +1,1323 @@
1
+ var WhaleStorefront = (function (exports, react, jsxRuntime) {
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
+ var NUM_PATTERN = /(\$?[\d,]+\.?\d*[+★%]?)/g;
349
+ function easeOutQuart(t) {
350
+ return 1 - Math.pow(1 - t, 4);
351
+ }
352
+ function useCountUp(target, duration, start) {
353
+ const [value, setValue] = react.useState(0);
354
+ const raf = react.useRef(0);
355
+ react.useEffect(() => {
356
+ if (!start) return;
357
+ const t0 = performance.now();
358
+ function tick(now) {
359
+ const elapsed = now - t0;
360
+ const progress = Math.min(elapsed / duration, 1);
361
+ setValue(Math.round(easeOutQuart(progress) * target));
362
+ if (progress < 1) raf.current = requestAnimationFrame(tick);
363
+ }
364
+ raf.current = requestAnimationFrame(tick);
365
+ return () => cancelAnimationFrame(raf.current);
366
+ }, [target, duration, start]);
367
+ return value;
368
+ }
369
+ function AnimatedNumber({ raw }) {
370
+ const ref = react.useRef(null);
371
+ const [visible, setVisible] = react.useState(false);
372
+ react.useEffect(() => {
373
+ const el = ref.current;
374
+ if (!el || typeof IntersectionObserver === "undefined") {
375
+ setVisible(true);
376
+ return;
377
+ }
378
+ const obs = new IntersectionObserver(([entry]) => {
379
+ if (entry.isIntersecting) {
380
+ setVisible(true);
381
+ obs.disconnect();
382
+ }
383
+ }, { threshold: 0.3 });
384
+ obs.observe(el);
385
+ return () => obs.disconnect();
386
+ }, []);
387
+ const prefix = raw.startsWith("$") ? "$" : "";
388
+ const suffix = raw.match(/[+★%]$/)?.[0] || "";
389
+ const numeric = parseFloat(raw.replace(/[\$,+★%]/g, ""));
390
+ const hasCommas = raw.includes(",");
391
+ const decimals = raw.includes(".") ? raw.split(".")[1]?.replace(/[+★%]/g, "").length || 0 : 0;
392
+ const count = useCountUp(
393
+ decimals > 0 ? Math.round(numeric * Math.pow(10, decimals)) : numeric,
394
+ 1400,
395
+ visible
396
+ );
397
+ const display = decimals > 0 ? (count / Math.pow(10, decimals)).toFixed(decimals) : hasCommas ? count.toLocaleString() : String(count);
398
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { ref, children: [
399
+ prefix,
400
+ display,
401
+ suffix
402
+ ] });
403
+ }
404
+ function AnimatedText({ text }) {
405
+ const parts = text.split(NUM_PATTERN);
406
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map(
407
+ (part, i) => NUM_PATTERN.test(part) ? /* @__PURE__ */ jsxRuntime.jsx(AnimatedNumber, { raw: part }, i) : part
408
+ ) });
409
+ }
410
+ function trackClick(tracking, label, url, position) {
411
+ if (!tracking?.gatewayUrl || !tracking?.code) return;
412
+ const body = JSON.stringify({ label, url, position });
413
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
414
+ navigator.sendBeacon(
415
+ `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
416
+ new Blob([body], { type: "application/json" })
417
+ );
418
+ }
419
+ }
420
+ function SectionRenderer({
421
+ section,
422
+ data,
423
+ theme,
424
+ tracking,
425
+ onEvent
426
+ }) {
427
+ const [showCOA, setShowCOA] = react.useState(false);
428
+ const el = (() => {
429
+ switch (section.type) {
430
+ case "hero":
431
+ return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme, tracking, onEvent });
432
+ case "text":
433
+ return /* @__PURE__ */ jsxRuntime.jsx(TextSection, { section, theme });
434
+ case "image":
435
+ return /* @__PURE__ */ jsxRuntime.jsx(ImageSection, { section, theme });
436
+ case "video":
437
+ return /* @__PURE__ */ jsxRuntime.jsx(VideoSection, { section, theme });
438
+ case "gallery":
439
+ return /* @__PURE__ */ jsxRuntime.jsx(GallerySection, { section, theme });
440
+ case "cta":
441
+ return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme, tracking, onEvent });
442
+ case "stats":
443
+ return /* @__PURE__ */ jsxRuntime.jsx(StatsSection, { section, theme });
444
+ case "product_card":
445
+ return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme, tracking });
446
+ case "coa_viewer":
447
+ return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
448
+ case "social_links":
449
+ return /* @__PURE__ */ jsxRuntime.jsx(SocialLinksSection, { section, theme });
450
+ case "lead_capture":
451
+ return /* @__PURE__ */ jsxRuntime.jsx(LeadCaptureSection, { section, data, theme, onEvent });
452
+ case "divider":
453
+ return /* @__PURE__ */ jsxRuntime.jsx(DividerSection, { theme });
454
+ default:
455
+ return null;
456
+ }
457
+ })();
458
+ const sectionRef = react.useRef(null);
459
+ react.useEffect(() => {
460
+ const el2 = sectionRef.current;
461
+ if (!el2 || typeof IntersectionObserver === "undefined") return;
462
+ const obs = new IntersectionObserver(
463
+ ([entry]) => {
464
+ if (entry.isIntersecting) {
465
+ onEvent?.("section_view", {
466
+ section_id: section.id,
467
+ section_type: section.type
468
+ });
469
+ obs.disconnect();
470
+ }
471
+ },
472
+ { threshold: 0.5 }
473
+ );
474
+ obs.observe(el2);
475
+ return () => obs.disconnect();
476
+ }, [section.id, section.type, onEvent]);
477
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
478
+ el,
479
+ showCOA && data?.coa && /* @__PURE__ */ jsxRuntime.jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
480
+ ] });
481
+ }
482
+ function HeroSection({ section, theme, tracking, onEvent }) {
483
+ const { title, subtitle, background_image, cta_text, cta_url } = section.content;
484
+ return /* @__PURE__ */ jsxRuntime.jsxs(
485
+ "div",
486
+ {
487
+ style: {
488
+ position: "relative",
489
+ minHeight: "60vh",
490
+ display: "flex",
491
+ flexDirection: "column",
492
+ justifyContent: "center",
493
+ alignItems: "center",
494
+ textAlign: "center",
495
+ padding: "3rem 1.5rem",
496
+ backgroundImage: background_image ? `url(${background_image})` : void 0,
497
+ backgroundSize: "cover",
498
+ backgroundPosition: "center"
499
+ },
500
+ children: [
501
+ background_image && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" } }),
502
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", zIndex: 1, maxWidth: 640 }, children: [
503
+ title && /* @__PURE__ */ jsxRuntime.jsx("h1", { style: {
504
+ fontSize: "clamp(2rem, 8vw, 3rem)",
505
+ fontWeight: 300,
506
+ fontFamily: theme.fontDisplay || "inherit",
507
+ margin: "0 0 1rem",
508
+ lineHeight: 1.15,
509
+ letterSpacing: "-0.02em",
510
+ color: theme.fg
511
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: title }) }),
512
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
513
+ fontSize: "0.85rem",
514
+ color: theme.accent,
515
+ margin: "0 0 2rem",
516
+ lineHeight: 1.6,
517
+ textTransform: "uppercase",
518
+ letterSpacing: "0.15em"
519
+ }, children: subtitle }),
520
+ cta_text && cta_url && /* @__PURE__ */ jsxRuntime.jsx(
521
+ "a",
522
+ {
523
+ href: cta_url,
524
+ onClick: () => {
525
+ trackClick(tracking, cta_text, cta_url);
526
+ onEvent?.("cta_click", { label: cta_text, url: cta_url });
527
+ },
528
+ style: {
529
+ display: "inline-block",
530
+ padding: "0.875rem 2rem",
531
+ background: theme.fg,
532
+ color: theme.bg,
533
+ textDecoration: "none",
534
+ fontSize: "0.85rem",
535
+ fontWeight: 500,
536
+ letterSpacing: "0.08em",
537
+ textTransform: "uppercase"
538
+ },
539
+ children: cta_text
540
+ }
541
+ )
542
+ ] })
543
+ ]
544
+ }
545
+ );
546
+ }
547
+ function TextSection({ section, theme }) {
548
+ const { heading, body } = section.content;
549
+ const align = section.config?.align || "left";
550
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 640, margin: "0 auto", textAlign: align }, children: [
551
+ heading && /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
552
+ fontSize: 11,
553
+ fontWeight: 500,
554
+ textTransform: "uppercase",
555
+ letterSpacing: "0.25em",
556
+ color: `${theme.fg}40`,
557
+ margin: "0 0 0.75rem"
558
+ }, children: heading }),
559
+ body && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: `${theme.fg}99`, lineHeight: 1.7, fontSize: "0.9rem", fontWeight: 300, whiteSpace: "pre-wrap" }, children: body })
560
+ ] });
561
+ }
562
+ function ImageSection({ section, theme }) {
563
+ const { url, alt, caption } = section.content;
564
+ const contained = section.config?.contained !== false;
565
+ if (!url) return null;
566
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: contained ? "1.5rem" : 0, maxWidth: contained ? 640 : void 0, margin: contained ? "0 auto" : void 0 }, children: [
567
+ /* @__PURE__ */ jsxRuntime.jsx(
568
+ "img",
569
+ {
570
+ src: url,
571
+ alt: alt || "",
572
+ style: { width: "100%", display: "block", objectFit: "cover" }
573
+ }
574
+ ),
575
+ caption && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: theme.muted, textAlign: "center", marginTop: "0.75rem" }, children: caption })
576
+ ] });
577
+ }
578
+ function VideoSection({ section, theme }) {
579
+ const { url, poster } = section.content;
580
+ if (!url) return null;
581
+ const isEmbed = url.includes("youtube") || url.includes("youtu.be") || url.includes("vimeo");
582
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: isEmbed ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", paddingBottom: "56.25%", height: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(
583
+ "iframe",
584
+ {
585
+ src: toEmbedUrl(url),
586
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: "none" },
587
+ allow: "autoplay; fullscreen",
588
+ title: "Video"
589
+ }
590
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(
591
+ "video",
592
+ {
593
+ src: url,
594
+ poster,
595
+ controls: true,
596
+ style: { width: "100%", display: "block", background: theme.surface }
597
+ }
598
+ ) });
599
+ }
600
+ function GallerySection({ section, theme }) {
601
+ const { images } = section.content;
602
+ const columns = section.config?.columns || 3;
603
+ if (!images || images.length === 0) return null;
604
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 800, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: "0.5rem" }, children: images.map((img, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { aspectRatio: "1", overflow: "hidden", background: theme.surface }, children: /* @__PURE__ */ jsxRuntime.jsx(
605
+ "img",
606
+ {
607
+ src: img.url,
608
+ alt: img.alt || "",
609
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
610
+ }
611
+ ) }, i)) }) });
612
+ }
613
+ function CTASection({ section, theme, tracking, onEvent }) {
614
+ const { title, subtitle, buttons } = section.content;
615
+ if (!buttons || buttons.length === 0) return null;
616
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
617
+ title && /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
618
+ fontSize: "clamp(1.25rem, 4vw, 1.5rem)",
619
+ fontWeight: 300,
620
+ fontFamily: theme.fontDisplay || "inherit",
621
+ margin: "0 0 0.25rem",
622
+ lineHeight: 1.2,
623
+ letterSpacing: "-0.02em",
624
+ color: theme.fg,
625
+ textAlign: "center"
626
+ }, children: title }),
627
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
628
+ fontSize: "0.8rem",
629
+ color: theme.accent,
630
+ margin: "0 0 0.75rem",
631
+ lineHeight: 1.6,
632
+ textTransform: "uppercase",
633
+ letterSpacing: "0.15em",
634
+ textAlign: "center"
635
+ }, children: subtitle }),
636
+ buttons.map((btn, i) => {
637
+ const isPrimary = btn.style !== "outline";
638
+ return /* @__PURE__ */ jsxRuntime.jsx(
639
+ "a",
640
+ {
641
+ href: btn.url,
642
+ onClick: () => {
643
+ trackClick(tracking, btn.text, btn.url, i);
644
+ onEvent?.("cta_click", { label: btn.text, url: btn.url });
645
+ },
646
+ style: {
647
+ display: "block",
648
+ width: "100%",
649
+ padding: "0.875rem",
650
+ background: isPrimary ? theme.fg : "transparent",
651
+ color: isPrimary ? theme.bg : theme.fg,
652
+ border: isPrimary ? "none" : `1px solid ${theme.fg}20`,
653
+ fontSize: "0.85rem",
654
+ fontWeight: 500,
655
+ textAlign: "center",
656
+ textDecoration: "none",
657
+ boxSizing: "border-box",
658
+ letterSpacing: "0.08em",
659
+ textTransform: "uppercase"
660
+ },
661
+ children: btn.text
662
+ },
663
+ i
664
+ );
665
+ })
666
+ ] });
667
+ }
668
+ function StatsSection({ section, theme }) {
669
+ const { stats } = section.content;
670
+ const layout = section.config?.layout;
671
+ if (!stats || stats.length === 0) return null;
672
+ if (layout === "list") {
673
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
674
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
675
+ display: "flex",
676
+ justifyContent: "space-between",
677
+ alignItems: "baseline",
678
+ padding: "0.625rem 0"
679
+ }, children: [
680
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
681
+ fontSize: 12,
682
+ textTransform: "uppercase",
683
+ letterSpacing: "0.15em",
684
+ color: `${theme.fg}66`
685
+ }, children: stat.label }),
686
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) })
687
+ ] }),
688
+ i < stats.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } })
689
+ ] }, i)) });
690
+ }
691
+ const columns = Math.min(stats.length, 4);
692
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
693
+ display: "grid",
694
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
695
+ border: `1px solid ${theme.fg}0F`
696
+ }, children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
697
+ padding: "1.25rem 0.5rem",
698
+ textAlign: "center",
699
+ borderRight: i < stats.length - 1 ? `1px solid ${theme.fg}0F` : void 0
700
+ }, children: [
701
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
702
+ fontFamily: theme.fontDisplay || "inherit",
703
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
704
+ fontWeight: 300,
705
+ lineHeight: 1,
706
+ color: theme.fg
707
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) }),
708
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
709
+ fontSize: 11,
710
+ fontWeight: 500,
711
+ textTransform: "uppercase",
712
+ letterSpacing: "0.25em",
713
+ color: theme.accent,
714
+ marginTop: "0.5rem"
715
+ }, children: stat.label })
716
+ ] }, i)) }) });
717
+ }
718
+ function ProductCardSection({ section, data, theme, tracking }) {
719
+ const product = data?.product;
720
+ const c = section.content;
721
+ const name = c.name || product?.name || "";
722
+ const description = c.description || product?.description || "";
723
+ const imageUrl = c.image_url || product?.featured_image || null;
724
+ const url = c.url || null;
725
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { background: theme.surface, overflow: "hidden" }, children: [
726
+ imageUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: "100%", aspectRatio: "1", overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: imageUrl, alt: name, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }),
727
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "1.25rem" }, children: [
728
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem", color: theme.fg }, children: name }),
729
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.9rem", color: theme.muted, margin: "0 0 1rem", lineHeight: 1.5 }, children: description }),
730
+ url && /* @__PURE__ */ jsxRuntime.jsx(
731
+ "a",
732
+ {
733
+ href: url,
734
+ onClick: () => trackClick(tracking, "View Product", url),
735
+ style: {
736
+ display: "block",
737
+ width: "100%",
738
+ padding: "0.75rem",
739
+ background: theme.fg,
740
+ color: theme.bg,
741
+ textAlign: "center",
742
+ textDecoration: "none",
743
+ fontSize: "0.85rem",
744
+ fontWeight: 500,
745
+ boxSizing: "border-box",
746
+ letterSpacing: "0.08em",
747
+ textTransform: "uppercase"
748
+ },
749
+ children: "View Product"
750
+ }
751
+ )
752
+ ] })
753
+ ] }) });
754
+ }
755
+ function COAViewerSection({
756
+ section,
757
+ data,
758
+ theme,
759
+ onShowCOA,
760
+ tracking
761
+ }) {
762
+ const coa = data?.coa;
763
+ const c = section.content;
764
+ if (!coa) return null;
765
+ const buttonStyle = {
766
+ width: "100%",
767
+ padding: "0.875rem",
768
+ background: theme.accent,
769
+ color: theme.bg,
770
+ border: "none",
771
+ fontSize: "0.85rem",
772
+ fontWeight: 500,
773
+ cursor: "pointer",
774
+ letterSpacing: "0.08em",
775
+ textTransform: "uppercase",
776
+ textAlign: "center",
777
+ textDecoration: "none",
778
+ display: "block",
779
+ boxSizing: "border-box"
780
+ };
781
+ const buttonLabel = c.button_text || "View Lab Results";
782
+ if (coa.viewer_url) {
783
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
784
+ "a",
785
+ {
786
+ href: coa.viewer_url,
787
+ target: "_blank",
788
+ rel: "noopener noreferrer",
789
+ onClick: () => trackClick(tracking, buttonLabel, coa.viewer_url),
790
+ style: buttonStyle,
791
+ children: buttonLabel
792
+ }
793
+ ) });
794
+ }
795
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => {
796
+ trackClick(tracking, buttonLabel, coa.url);
797
+ onShowCOA();
798
+ }, style: buttonStyle, children: buttonLabel }) });
799
+ }
800
+ function LeadCaptureSection({ section, data, theme, onEvent }) {
801
+ const c = section.content;
802
+ const [firstName, setFirstName] = react.useState("");
803
+ const [email, setEmail] = react.useState("");
804
+ const [newsletterOptIn, setNewsletterOptIn] = react.useState(false);
805
+ const [status, setStatus] = react.useState("idle");
806
+ const [errorMsg, setErrorMsg] = react.useState("");
807
+ const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
808
+ const storeId = c.store_id || data.store?.id;
809
+ const slug = c.landing_page_slug || data.landing_page?.slug;
810
+ async function handleSubmit(e) {
811
+ e.preventDefault();
812
+ if (!email || !storeId) return;
813
+ setStatus("loading");
814
+ setErrorMsg("");
815
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
816
+ const analyticsData = data.analyticsContext;
817
+ try {
818
+ const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
819
+ method: "POST",
820
+ headers: { "Content-Type": "application/json" },
821
+ body: JSON.stringify({
822
+ email,
823
+ first_name: firstName || void 0,
824
+ source: c.source || "landing_page",
825
+ landing_page_slug: slug || void 0,
826
+ newsletter_opt_in: newsletterOptIn || void 0,
827
+ tags: (() => {
828
+ const t = [...c.tags || []];
829
+ if (newsletterOptIn) t.push(c.newsletter_tag || "newsletter-subscriber");
830
+ return t.length > 0 ? t : void 0;
831
+ })(),
832
+ visitor_id: analyticsData?.visitorId || void 0,
833
+ session_id: analyticsData?.sessionId || void 0,
834
+ utm_source: urlParams?.get("utm_source") || void 0,
835
+ utm_medium: urlParams?.get("utm_medium") || void 0,
836
+ utm_campaign: urlParams?.get("utm_campaign") || void 0,
837
+ utm_content: urlParams?.get("utm_content") || void 0
838
+ })
839
+ });
840
+ if (!res.ok) {
841
+ const body = await res.json().catch(() => ({}));
842
+ throw new Error(body?.error?.message || "Something went wrong. Please try again.");
843
+ }
844
+ setStatus("success");
845
+ onEvent?.("lead", { email, first_name: firstName || void 0, source: c.source || "landing_page", landing_page_slug: slug || void 0 });
846
+ } catch (err) {
847
+ setErrorMsg(err instanceof Error ? err.message : "Something went wrong. Please try again.");
848
+ setStatus("error");
849
+ }
850
+ }
851
+ const heading = c.heading || "get 10% off your first visit.";
852
+ const subtitle = c.subtitle || "drop your email and we will send you the code.";
853
+ const buttonText = c.button_text || "Claim My Discount";
854
+ const successHeading = c.success_heading || "You\u2019re in!";
855
+ const successMessage = c.success_message || "Check your inbox for the discount code.";
856
+ const inputStyle = {
857
+ flex: 1,
858
+ minWidth: 0,
859
+ padding: "0.875rem 1rem",
860
+ background: theme.surface,
861
+ border: `1px solid ${theme.fg}15`,
862
+ color: theme.fg,
863
+ fontSize: "0.95rem",
864
+ fontWeight: 300,
865
+ outline: "none",
866
+ boxSizing: "border-box",
867
+ fontFamily: "inherit",
868
+ transition: "border-color 0.2s"
869
+ };
870
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
871
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
872
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
873
+ background: theme.surface,
874
+ border: `1px solid ${theme.fg}12`,
875
+ padding: "clamp(2rem, 6vw, 3rem)"
876
+ }, children: status === "success" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center" }, children: [
877
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
878
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
879
+ fontWeight: 300,
880
+ fontFamily: theme.fontDisplay || "inherit",
881
+ margin: "0 0 0.75rem",
882
+ lineHeight: 1.2,
883
+ letterSpacing: "-0.02em",
884
+ color: theme.fg
885
+ }, children: successHeading }),
886
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
887
+ fontSize: "0.9rem",
888
+ color: `${theme.fg}99`,
889
+ margin: "0 0 1.5rem",
890
+ lineHeight: 1.6,
891
+ fontWeight: 300
892
+ }, children: successMessage }),
893
+ c.coupon_code && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
894
+ display: "inline-block",
895
+ padding: "0.75rem 2rem",
896
+ background: `${theme.fg}08`,
897
+ border: `1px dashed ${theme.fg}30`,
898
+ fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
899
+ fontWeight: 500,
900
+ fontFamily: "monospace",
901
+ letterSpacing: "0.12em",
902
+ color: theme.accent
903
+ }, children: c.coupon_code })
904
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
905
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
906
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
907
+ fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
908
+ fontWeight: 300,
909
+ fontFamily: theme.fontDisplay || "inherit",
910
+ margin: "0 0 0.5rem",
911
+ lineHeight: 1.15,
912
+ letterSpacing: "-0.02em",
913
+ color: theme.fg
914
+ }, children: heading }),
915
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
916
+ fontSize: "0.85rem",
917
+ color: theme.accent,
918
+ margin: 0,
919
+ lineHeight: 1.6,
920
+ textTransform: "uppercase",
921
+ letterSpacing: "0.15em"
922
+ }, children: subtitle })
923
+ ] }),
924
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
925
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
926
+ /* @__PURE__ */ jsxRuntime.jsx(
927
+ "input",
928
+ {
929
+ type: "text",
930
+ placeholder: "First name",
931
+ value: firstName,
932
+ onChange: (e) => setFirstName(e.target.value),
933
+ style: inputStyle
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsxRuntime.jsx(
937
+ "input",
938
+ {
939
+ type: "email",
940
+ placeholder: "Email address",
941
+ value: email,
942
+ onChange: (e) => setEmail(e.target.value),
943
+ required: true,
944
+ style: inputStyle
945
+ }
946
+ )
947
+ ] }),
948
+ c.show_newsletter_opt_in !== false && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: {
949
+ display: "flex",
950
+ alignItems: "center",
951
+ gap: "0.5rem",
952
+ cursor: "pointer",
953
+ fontSize: "0.8rem",
954
+ color: `${theme.fg}90`,
955
+ fontWeight: 300,
956
+ lineHeight: 1.4
957
+ }, children: [
958
+ /* @__PURE__ */ jsxRuntime.jsx(
959
+ "input",
960
+ {
961
+ type: "checkbox",
962
+ checked: newsletterOptIn,
963
+ onChange: (e) => setNewsletterOptIn(e.target.checked),
964
+ style: {
965
+ width: 16,
966
+ height: 16,
967
+ accentColor: theme.accent,
968
+ cursor: "pointer",
969
+ flexShrink: 0
970
+ }
971
+ }
972
+ ),
973
+ c.newsletter_label || "Also sign me up for the newsletter \u2014 new drops, deals, and company news."
974
+ ] }),
975
+ status === "error" && errorMsg && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
976
+ /* @__PURE__ */ jsxRuntime.jsxs(
977
+ "button",
978
+ {
979
+ type: "submit",
980
+ disabled: status === "loading",
981
+ style: {
982
+ width: "100%",
983
+ padding: "0.875rem",
984
+ background: theme.fg,
985
+ color: theme.bg,
986
+ border: "none",
987
+ fontSize: "0.85rem",
988
+ fontWeight: 500,
989
+ cursor: status === "loading" ? "wait" : "pointer",
990
+ letterSpacing: "0.08em",
991
+ textTransform: "uppercase",
992
+ fontFamily: "inherit",
993
+ display: "flex",
994
+ alignItems: "center",
995
+ justifyContent: "center",
996
+ gap: "0.5rem",
997
+ opacity: status === "loading" ? 0.7 : 1,
998
+ transition: "opacity 0.2s"
999
+ },
1000
+ children: [
1001
+ status === "loading" && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
1002
+ display: "inline-block",
1003
+ width: 16,
1004
+ height: 16,
1005
+ border: `2px solid ${theme.bg}40`,
1006
+ borderTopColor: theme.bg,
1007
+ borderRadius: "50%",
1008
+ animation: "lc-spin 0.8s linear infinite"
1009
+ } }),
1010
+ buttonText
1011
+ ]
1012
+ }
1013
+ )
1014
+ ] })
1015
+ ] }) })
1016
+ ] });
1017
+ }
1018
+ function SocialLinksSection({ section, theme }) {
1019
+ const { links } = section.content;
1020
+ if (!links || links.length === 0) return null;
1021
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center", gap: "1.5rem", flexWrap: "wrap" }, children: links.map((link, i) => /* @__PURE__ */ jsxRuntime.jsx(
1022
+ "a",
1023
+ {
1024
+ href: link.url,
1025
+ target: "_blank",
1026
+ rel: "noopener noreferrer",
1027
+ style: {
1028
+ color: theme.muted,
1029
+ textDecoration: "none",
1030
+ fontSize: "0.85rem",
1031
+ fontWeight: 500,
1032
+ textTransform: "capitalize",
1033
+ letterSpacing: "0.03em"
1034
+ },
1035
+ children: link.platform
1036
+ },
1037
+ i
1038
+ )) });
1039
+ }
1040
+ function DividerSection({ theme }) {
1041
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1rem 1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } }) });
1042
+ }
1043
+ function COAModal({ coa, theme, onClose }) {
1044
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.95)", display: "flex", flexDirection: "column" }, children: [
1045
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
1046
+ display: "flex",
1047
+ justifyContent: "space-between",
1048
+ alignItems: "center",
1049
+ padding: "0.75rem 1rem",
1050
+ borderBottom: `1px solid ${theme.fg}10`
1051
+ }, children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#fff", fontWeight: 500, fontSize: "0.85rem" }, children: coa.document_name || "Lab Results" }),
1053
+ /* @__PURE__ */ jsxRuntime.jsx(
1054
+ "button",
1055
+ {
1056
+ onClick: onClose,
1057
+ style: {
1058
+ background: `${theme.fg}10`,
1059
+ border: "none",
1060
+ color: "#fff",
1061
+ fontSize: "0.85rem",
1062
+ cursor: "pointer",
1063
+ padding: "0.375rem 0.75rem"
1064
+ },
1065
+ children: "Close"
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: coa.url, style: { flex: 1, border: "none", background: "#fff" }, title: "Lab Results" })
1070
+ ] });
1071
+ }
1072
+ function toEmbedUrl(url) {
1073
+ const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
1074
+ if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
1075
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
1076
+ if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
1077
+ return url;
1078
+ }
1079
+ function LandingPage({
1080
+ slug,
1081
+ gatewayUrl = "https://whale-gateway.fly.dev",
1082
+ renderSection,
1083
+ onDataLoaded,
1084
+ onError,
1085
+ onEvent,
1086
+ analyticsContext,
1087
+ enableAnalytics = true
1088
+ }) {
1089
+ const [state, setState] = react.useState("loading");
1090
+ const [data, setData] = react.useState(null);
1091
+ const [errorMsg, setErrorMsg] = react.useState("");
1092
+ react.useEffect(() => {
1093
+ if (!slug) return;
1094
+ let cancelled = false;
1095
+ if (typeof window !== "undefined" && window.__LANDING_DATA__) {
1096
+ const json = window.__LANDING_DATA__;
1097
+ setData(json);
1098
+ setState("ready");
1099
+ onDataLoaded?.(json);
1100
+ return;
1101
+ }
1102
+ async function load() {
1103
+ try {
1104
+ const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
1105
+ if (!cancelled) {
1106
+ if (res.status === 404) {
1107
+ setState("not_found");
1108
+ return;
1109
+ }
1110
+ if (res.status === 410) {
1111
+ setState("expired");
1112
+ return;
1113
+ }
1114
+ if (!res.ok) {
1115
+ const body = await res.json().catch(() => ({}));
1116
+ throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1117
+ }
1118
+ const json = await res.json();
1119
+ setData(json);
1120
+ setState("ready");
1121
+ onDataLoaded?.(json);
1122
+ }
1123
+ } catch (err) {
1124
+ if (!cancelled) {
1125
+ const e = err instanceof Error ? err : new Error(String(err));
1126
+ setErrorMsg(e.message);
1127
+ setState("error");
1128
+ onError?.(e);
1129
+ }
1130
+ }
1131
+ }
1132
+ load();
1133
+ return () => {
1134
+ cancelled = true;
1135
+ };
1136
+ }, [slug, gatewayUrl]);
1137
+ if (state === "loading") return /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {});
1138
+ if (state === "not_found") return /* @__PURE__ */ jsxRuntime.jsx(DefaultNotFound, {});
1139
+ if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired, {});
1140
+ if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError, { message: errorMsg });
1141
+ if (!data) return null;
1142
+ return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext, enableAnalytics });
1143
+ }
1144
+ function isSectionVisible(section, urlParams) {
1145
+ const vis = section.config?.visibility;
1146
+ if (!vis?.params) return true;
1147
+ for (const [key, allowed] of Object.entries(vis.params)) {
1148
+ const val = urlParams.get(key);
1149
+ if (!val || !allowed.includes(val)) return false;
1150
+ }
1151
+ return true;
1152
+ }
1153
+ function PageLayout({
1154
+ data,
1155
+ gatewayUrl,
1156
+ renderSection,
1157
+ onEvent,
1158
+ analyticsContext,
1159
+ enableAnalytics
1160
+ }) {
1161
+ const { landing_page: lp, store } = data;
1162
+ const trackerRef = react.useRef(null);
1163
+ react.useEffect(() => {
1164
+ if (!enableAnalytics || typeof window === "undefined") return;
1165
+ const analyticsConfig = window.__LANDING_ANALYTICS__;
1166
+ if (!analyticsConfig?.slug) return;
1167
+ let visitorId = localStorage.getItem("wt_vid") || "";
1168
+ if (!visitorId) {
1169
+ visitorId = crypto.randomUUID();
1170
+ localStorage.setItem("wt_vid", visitorId);
1171
+ }
1172
+ let sessionId = sessionStorage.getItem("wt_sid") || "";
1173
+ if (!sessionId) {
1174
+ sessionId = crypto.randomUUID();
1175
+ sessionStorage.setItem("wt_sid", sessionId);
1176
+ }
1177
+ Promise.resolve().then(() => (init_tracker(), tracker_exports)).then(({ BehavioralTracker: BehavioralTracker2 }) => {
1178
+ const gwUrl = analyticsConfig.gatewayUrl || gatewayUrl;
1179
+ const slug = analyticsConfig.slug;
1180
+ const utmParams = new URLSearchParams(window.location.search);
1181
+ const tracker = new BehavioralTracker2({
1182
+ sessionId,
1183
+ visitorId,
1184
+ sendBatch: async (batch) => {
1185
+ const events = batch.events.map((e) => ({
1186
+ event_type: e.data_type,
1187
+ event_data: e.data,
1188
+ session_id: batch.session_id,
1189
+ visitor_id: batch.visitor_id,
1190
+ campaign_id: analyticsConfig.campaignId || utmParams.get("utm_campaign_id") || void 0,
1191
+ utm_source: utmParams.get("utm_source") || void 0,
1192
+ utm_medium: utmParams.get("utm_medium") || void 0,
1193
+ utm_campaign: utmParams.get("utm_campaign") || void 0
1194
+ }));
1195
+ const body = JSON.stringify({ events });
1196
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1197
+ navigator.sendBeacon(
1198
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1199
+ new Blob([body], { type: "application/json" })
1200
+ );
1201
+ } else {
1202
+ await fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1203
+ method: "POST",
1204
+ headers: { "Content-Type": "application/json" },
1205
+ body,
1206
+ keepalive: true
1207
+ });
1208
+ }
1209
+ }
1210
+ });
1211
+ tracker.setPageContext(window.location.href, window.location.pathname);
1212
+ tracker.start();
1213
+ trackerRef.current = tracker;
1214
+ const pageViewBody = JSON.stringify({
1215
+ events: [{
1216
+ event_type: "page_view",
1217
+ event_data: { referrer: document.referrer, url: window.location.href },
1218
+ session_id: sessionId,
1219
+ visitor_id: visitorId,
1220
+ campaign_id: analyticsConfig.campaignId || void 0,
1221
+ utm_source: utmParams.get("utm_source") || void 0,
1222
+ utm_medium: utmParams.get("utm_medium") || void 0,
1223
+ utm_campaign: utmParams.get("utm_campaign") || void 0
1224
+ }]
1225
+ });
1226
+ if (navigator.sendBeacon) {
1227
+ navigator.sendBeacon(
1228
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1229
+ new Blob([pageViewBody], { type: "application/json" })
1230
+ );
1231
+ } else {
1232
+ fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1233
+ method: "POST",
1234
+ headers: { "Content-Type": "application/json" },
1235
+ body: pageViewBody,
1236
+ keepalive: true
1237
+ }).catch(() => {
1238
+ });
1239
+ }
1240
+ }).catch(() => {
1241
+ });
1242
+ return () => {
1243
+ if (trackerRef.current) {
1244
+ trackerRef.current.stop();
1245
+ trackerRef.current = null;
1246
+ }
1247
+ };
1248
+ }, [enableAnalytics, gatewayUrl]);
1249
+ const theme = {
1250
+ bg: lp.background_color || store?.theme?.background || "#050505",
1251
+ fg: lp.text_color || store?.theme?.foreground || "#fafafa",
1252
+ accent: lp.accent_color || store?.theme?.accent || "#E8E2D9",
1253
+ surface: store?.theme?.surface || "#111",
1254
+ muted: store?.theme?.muted || "#888",
1255
+ fontDisplay: store?.theme?.fontDisplay || void 0,
1256
+ fontBody: store?.theme?.fontBody || void 0
1257
+ };
1258
+ const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
1259
+ const logoUrl = store?.logo_url;
1260
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : new URLSearchParams();
1261
+ const sorted = [...lp.sections].sort((a, b) => a.order - b.order).filter((s) => isSectionVisible(s, urlParams));
1262
+ const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
1263
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1264
+ lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
1265
+ 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" } }) }),
1266
+ sorted.map((section) => {
1267
+ const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1268
+ if (renderSection) {
1269
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1270
+ }
1271
+ return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1272
+ }),
1273
+ store?.name && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1274
+ "Powered by ",
1275
+ store.name
1276
+ ] }) })
1277
+ ] });
1278
+ }
1279
+ var containerStyle = {
1280
+ minHeight: "100dvh",
1281
+ display: "flex",
1282
+ justifyContent: "center",
1283
+ alignItems: "center",
1284
+ fontFamily: "system-ui, -apple-system, sans-serif",
1285
+ background: "#050505",
1286
+ color: "#fafafa",
1287
+ textAlign: "center",
1288
+ padding: "2rem"
1289
+ };
1290
+ function DefaultLoading() {
1291
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1292
+ /* @__PURE__ */ jsxRuntime.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" } }),
1293
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1294
+ ] }) });
1295
+ }
1296
+ function DefaultNotFound() {
1297
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1298
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Not Found" }),
1299
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page does not exist or has been removed." })
1300
+ ] }) });
1301
+ }
1302
+ function DefaultExpired() {
1303
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1304
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Expired" }),
1305
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page is no longer active." })
1306
+ ] }) });
1307
+ }
1308
+ function DefaultError({ message }) {
1309
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1310
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Something Went Wrong" }),
1311
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: message || "Please try again later." })
1312
+ ] }) });
1313
+ }
1314
+
1315
+ // src/landing-entry.ts
1316
+ init_tracker();
1317
+
1318
+ exports.LandingPage = LandingPage;
1319
+ exports.SectionRenderer = SectionRenderer;
1320
+
1321
+ return exports;
1322
+
1323
+ })({}, react, jsxRuntime);