@neowhale/storefront 0.2.4 → 0.2.7

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.
@@ -1,13 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkBTGOSNMP_cjs = require('../chunk-BTGOSNMP.cjs');
4
- var chunkPVO5ZGBN_cjs = require('../chunk-PVO5ZGBN.cjs');
4
+ var chunk3VKRKDPL_cjs = require('../chunk-3VKRKDPL.cjs');
5
5
  var react = require('react');
6
6
  var navigation = require('next/navigation');
7
7
  var vanilla = require('zustand/vanilla');
8
8
  var middleware = require('zustand/middleware');
9
9
  var zustand = require('zustand');
10
10
  var shallow = require('zustand/react/shallow');
11
+ var ui = require('@neowhale/ui');
11
12
  var jsxRuntime = require('react/jsx-runtime');
12
13
 
13
14
  var WhaleContext = react.createContext(null);
@@ -536,23 +537,26 @@ function AuthInitializer() {
536
537
  }, []);
537
538
  return null;
538
539
  }
539
- function PixelInitializer({ onReady }) {
540
+ function PixelInitializer({ onReady, onTheme }) {
540
541
  const ctx = react.useContext(WhaleContext);
541
542
  const initialized = react.useRef(false);
542
543
  react.useEffect(() => {
543
544
  if (!ctx || initialized.current) return;
544
545
  if (typeof window === "undefined") return;
545
- if (!ctx.config.trackingEnabled) return;
546
546
  initialized.current = true;
547
547
  const { client } = ctx;
548
548
  client.fetchStorefrontConfig().then(async (config) => {
549
- if (!config.pixels || config.pixels.length === 0) return;
550
- const manager = new chunkBTGOSNMP_cjs.PixelManager(config.pixels);
551
- await manager.initialize();
552
- onReady(manager);
549
+ if (config.theme && onTheme) {
550
+ onTheme(config.theme);
551
+ }
552
+ if (ctx.config.trackingEnabled && config.pixels && config.pixels.length > 0) {
553
+ const manager = new chunkBTGOSNMP_cjs.PixelManager(config.pixels);
554
+ await manager.initialize();
555
+ onReady(manager);
556
+ }
553
557
  }).catch(() => {
554
558
  });
555
- }, [ctx, onReady]);
559
+ }, [ctx, onReady, onTheme]);
556
560
  return null;
557
561
  }
558
562
  function envBool(name) {
@@ -571,6 +575,7 @@ function envNumber(name) {
571
575
  function WhaleProvider({
572
576
  children,
573
577
  products = [],
578
+ theme: themeProp,
574
579
  storeId,
575
580
  apiKey,
576
581
  gatewayUrl,
@@ -585,9 +590,13 @@ function WhaleProvider({
585
590
  }) {
586
591
  const pathname = navigation.usePathname();
587
592
  const [pixelManager, setPixelManager] = react.useState(null);
593
+ const [storeTheme, setStoreTheme] = react.useState(void 0);
588
594
  const handlePixelReady = react.useCallback((manager) => {
589
595
  setPixelManager(manager);
590
596
  }, []);
597
+ const handleTheme = react.useCallback((theme) => {
598
+ setStoreTheme(theme);
599
+ }, []);
591
600
  const ctx = react.useMemo(() => {
592
601
  const resolvedConfig = {
593
602
  storeId,
@@ -602,7 +611,7 @@ function WhaleProvider({
602
611
  trackingEnabled: trackingEnabled ?? envBool("NEXT_PUBLIC_TRACKING_ENABLED") ?? true,
603
612
  recordingRate: recordingRate ?? envNumber("NEXT_PUBLIC_RECORDING_RATE") ?? 0.1
604
613
  };
605
- const client = new chunkPVO5ZGBN_cjs.WhaleClient({
614
+ const client = new chunk3VKRKDPL_cjs.WhaleClient({
606
615
  storeId,
607
616
  apiKey,
608
617
  gatewayUrl: resolvedConfig.gatewayUrl,
@@ -623,13 +632,17 @@ function WhaleProvider({
623
632
  () => ({ ...ctx, products, pixelManager }),
624
633
  [ctx, products, pixelManager]
625
634
  );
626
- return /* @__PURE__ */ jsxRuntime.jsxs(WhaleContext.Provider, { value, children: [
635
+ const mergedTheme = react.useMemo(() => {
636
+ if (!storeTheme && !themeProp) return void 0;
637
+ return { ...storeTheme, ...themeProp };
638
+ }, [storeTheme, themeProp]);
639
+ return /* @__PURE__ */ jsxRuntime.jsx(WhaleContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ThemeProvider, { theme: mergedTheme, children: [
627
640
  /* @__PURE__ */ jsxRuntime.jsx(AuthInitializer, {}),
628
641
  /* @__PURE__ */ jsxRuntime.jsx(CartInitializer, {}),
629
642
  /* @__PURE__ */ jsxRuntime.jsx(AnalyticsTracker, { pathname }),
630
- /* @__PURE__ */ jsxRuntime.jsx(PixelInitializer, { onReady: handlePixelReady }),
643
+ /* @__PURE__ */ jsxRuntime.jsx(PixelInitializer, { onReady: handlePixelReady, onTheme: handleTheme }),
631
644
  children
632
- ] });
645
+ ] }) });
633
646
  }
634
647
  function useProducts(opts) {
635
648
  const ctx = react.useContext(WhaleContext);
@@ -714,11 +727,1216 @@ function useCustomerAnalytics() {
714
727
  }, [customer?.id, customer?.first_name, customer?.last_name, ctx.client]);
715
728
  return { analytics, loading };
716
729
  }
730
+ function useCheckout() {
731
+ const ctx = react.useContext(WhaleContext);
732
+ if (!ctx) throw new Error("useCheckout must be used within <WhaleProvider>");
733
+ const [session, setSession] = react.useState(null);
734
+ const [loading, setLoading] = react.useState(false);
735
+ const [error, setError] = react.useState(null);
736
+ const createSession = react.useCallback(async (params) => {
737
+ setLoading(true);
738
+ setError(null);
739
+ try {
740
+ const data = await ctx.client.createCheckoutSession(params);
741
+ setSession(data);
742
+ return data;
743
+ } catch (err) {
744
+ const e = err instanceof Error ? err : new Error(String(err));
745
+ setError(e);
746
+ throw e;
747
+ } finally {
748
+ setLoading(false);
749
+ }
750
+ }, [ctx.client]);
751
+ const updateSession = react.useCallback(async (params) => {
752
+ if (!session) throw new Error("No active checkout session");
753
+ setLoading(true);
754
+ setError(null);
755
+ try {
756
+ const data = await ctx.client.updateCheckoutSession(session.id, params);
757
+ setSession(data);
758
+ return data;
759
+ } catch (err) {
760
+ const e = err instanceof Error ? err : new Error(String(err));
761
+ setError(e);
762
+ throw e;
763
+ } finally {
764
+ setLoading(false);
765
+ }
766
+ }, [ctx.client, session]);
767
+ const complete = react.useCallback(async (payment) => {
768
+ if (!session) throw new Error("No active checkout session");
769
+ setLoading(true);
770
+ setError(null);
771
+ try {
772
+ const order = await ctx.client.completeCheckout(session.id, payment);
773
+ setSession(null);
774
+ return order;
775
+ } catch (err) {
776
+ const e = err instanceof Error ? err : new Error(String(err));
777
+ setError(e);
778
+ throw e;
779
+ } finally {
780
+ setLoading(false);
781
+ }
782
+ }, [ctx.client, session]);
783
+ const reset = react.useCallback(() => {
784
+ setSession(null);
785
+ setError(null);
786
+ }, []);
787
+ return { session, loading, error, createSession, updateSession, complete, reset };
788
+ }
789
+ function useSearch() {
790
+ const ctx = react.useContext(WhaleContext);
791
+ if (!ctx) throw new Error("useSearch must be used within <WhaleProvider>");
792
+ const [results, setResults] = react.useState([]);
793
+ const [hasMore, setHasMore] = react.useState(false);
794
+ const [loading, setLoading] = react.useState(false);
795
+ const [error, setError] = react.useState(null);
796
+ const [cursor, setCursor] = react.useState(void 0);
797
+ const search = react.useCallback(async (params) => {
798
+ setLoading(true);
799
+ setError(null);
800
+ try {
801
+ const data = await ctx.client.searchProducts(params);
802
+ setResults(data.data);
803
+ setHasMore(data.has_more);
804
+ setCursor(data.data.length > 0 ? data.data[data.data.length - 1].id : void 0);
805
+ } catch (err) {
806
+ const e = err instanceof Error ? err : new Error(String(err));
807
+ setError(e);
808
+ setResults([]);
809
+ setHasMore(false);
810
+ } finally {
811
+ setLoading(false);
812
+ }
813
+ }, [ctx.client]);
814
+ const loadMore = react.useCallback(async (params) => {
815
+ if (!cursor || !hasMore) return;
816
+ setLoading(true);
817
+ setError(null);
818
+ try {
819
+ const data = await ctx.client.searchProducts({ ...params, starting_after: cursor });
820
+ setResults((prev) => [...prev, ...data.data]);
821
+ setHasMore(data.has_more);
822
+ setCursor(data.data.length > 0 ? data.data[data.data.length - 1].id : void 0);
823
+ } catch (err) {
824
+ const e = err instanceof Error ? err : new Error(String(err));
825
+ setError(e);
826
+ } finally {
827
+ setLoading(false);
828
+ }
829
+ }, [ctx.client, cursor, hasMore]);
830
+ const clear = react.useCallback(() => {
831
+ setResults([]);
832
+ setHasMore(false);
833
+ setCursor(void 0);
834
+ setError(null);
835
+ }, []);
836
+ return { results, hasMore, loading, error, search, loadMore, clear };
837
+ }
838
+ function buildTree(categories) {
839
+ const map = /* @__PURE__ */ new Map();
840
+ const roots = [];
841
+ for (const cat of categories) {
842
+ map.set(cat.id, { ...cat, children: [] });
843
+ }
844
+ for (const cat of categories) {
845
+ const node = map.get(cat.id);
846
+ if (cat.parent_id && map.has(cat.parent_id)) {
847
+ map.get(cat.parent_id).children.push(node);
848
+ } else {
849
+ roots.push(node);
850
+ }
851
+ }
852
+ return roots;
853
+ }
854
+ function useCategories() {
855
+ const ctx = react.useContext(WhaleContext);
856
+ if (!ctx) throw new Error("useCategories must be used within <WhaleProvider>");
857
+ const [categories, setCategories] = react.useState([]);
858
+ const [loading, setLoading] = react.useState(true);
859
+ const [error, setError] = react.useState(null);
860
+ const refresh = react.useCallback(async () => {
861
+ setLoading(true);
862
+ setError(null);
863
+ try {
864
+ const data = await ctx.client.listCategories();
865
+ setCategories(data.data);
866
+ } catch (err) {
867
+ const e = err instanceof Error ? err : new Error(String(err));
868
+ setError(e);
869
+ setCategories([]);
870
+ } finally {
871
+ setLoading(false);
872
+ }
873
+ }, [ctx.client]);
874
+ react.useEffect(() => {
875
+ refresh();
876
+ }, [refresh]);
877
+ const tree = react.useMemo(() => buildTree(categories), [categories]);
878
+ const getCategory = react.useCallback(async (id) => {
879
+ return ctx.client.getCategory(id);
880
+ }, [ctx.client]);
881
+ return { categories, tree, loading, error, refresh, getCategory };
882
+ }
883
+ function useLoyalty() {
884
+ const ctx = react.useContext(WhaleContext);
885
+ if (!ctx) throw new Error("useLoyalty must be used within <WhaleProvider>");
886
+ const customer = zustand.useStore(ctx.authStore, (s) => s.customer);
887
+ const [account, setAccount] = react.useState(null);
888
+ const [rewards, setRewards] = react.useState([]);
889
+ const [transactions, setTransactions] = react.useState([]);
890
+ const [loading, setLoading] = react.useState(false);
891
+ const [error, setError] = react.useState(null);
892
+ const refresh = react.useCallback(async () => {
893
+ if (!customer?.id) {
894
+ setAccount(null);
895
+ setRewards([]);
896
+ setTransactions([]);
897
+ return;
898
+ }
899
+ setLoading(true);
900
+ setError(null);
901
+ try {
902
+ const [acct, rwds, txns] = await Promise.all([
903
+ ctx.client.getLoyaltyAccount(customer.id),
904
+ ctx.client.listLoyaltyRewards(),
905
+ ctx.client.listLoyaltyTransactions(customer.id)
906
+ ]);
907
+ setAccount(acct);
908
+ setRewards(rwds.data);
909
+ setTransactions(txns.data);
910
+ } catch (err) {
911
+ const e = err instanceof Error ? err : new Error(String(err));
912
+ setError(e);
913
+ } finally {
914
+ setLoading(false);
915
+ }
916
+ }, [customer?.id, ctx.client]);
917
+ react.useEffect(() => {
918
+ refresh();
919
+ }, [refresh]);
920
+ const redeemReward = react.useCallback(async (rewardId) => {
921
+ if (!customer?.id) throw new Error("Not authenticated");
922
+ const result = await ctx.client.redeemLoyaltyReward(customer.id, rewardId);
923
+ await refresh();
924
+ return result;
925
+ }, [customer?.id, ctx.client, refresh]);
926
+ return { account, rewards, transactions, loading, error, refresh, redeemReward };
927
+ }
928
+ function useReviews(productId) {
929
+ const ctx = react.useContext(WhaleContext);
930
+ if (!ctx) throw new Error("useReviews must be used within <WhaleProvider>");
931
+ const [reviews, setReviews] = react.useState([]);
932
+ const [summary, setSummary] = react.useState(null);
933
+ const [hasMore, setHasMore] = react.useState(false);
934
+ const [loading, setLoading] = react.useState(false);
935
+ const [error, setError] = react.useState(null);
936
+ const refresh = react.useCallback(async () => {
937
+ if (!productId) {
938
+ setReviews([]);
939
+ setSummary(null);
940
+ return;
941
+ }
942
+ setLoading(true);
943
+ setError(null);
944
+ try {
945
+ const data = await ctx.client.listProductReviews(productId);
946
+ setReviews(data.data);
947
+ setSummary(data.summary ?? null);
948
+ setHasMore(data.has_more);
949
+ } catch (err) {
950
+ const e = err instanceof Error ? err : new Error(String(err));
951
+ setError(e);
952
+ setReviews([]);
953
+ setSummary(null);
954
+ } finally {
955
+ setLoading(false);
956
+ }
957
+ }, [productId, ctx.client]);
958
+ react.useEffect(() => {
959
+ refresh();
960
+ }, [refresh]);
961
+ const loadMore = react.useCallback(async () => {
962
+ if (!productId || !hasMore || reviews.length === 0) return;
963
+ setLoading(true);
964
+ setError(null);
965
+ try {
966
+ const cursor = reviews[reviews.length - 1].id;
967
+ const data = await ctx.client.listProductReviews(productId, { starting_after: cursor });
968
+ setReviews((prev) => [...prev, ...data.data]);
969
+ setHasMore(data.has_more);
970
+ } catch (err) {
971
+ const e = err instanceof Error ? err : new Error(String(err));
972
+ setError(e);
973
+ } finally {
974
+ setLoading(false);
975
+ }
976
+ }, [productId, ctx.client, hasMore, reviews]);
977
+ const submit = react.useCallback(async (data) => {
978
+ if (!productId) throw new Error("No product ID provided");
979
+ const review = await ctx.client.submitReview(productId, data);
980
+ await refresh();
981
+ return review;
982
+ }, [productId, ctx.client, refresh]);
983
+ return { reviews, summary, hasMore, loading, error, refresh, loadMore, submit };
984
+ }
985
+ function useWishlist() {
986
+ const ctx = react.useContext(WhaleContext);
987
+ if (!ctx) throw new Error("useWishlist must be used within <WhaleProvider>");
988
+ const customer = zustand.useStore(ctx.authStore, (s) => s.customer);
989
+ const [items, setItems] = react.useState([]);
990
+ const [loading, setLoading] = react.useState(false);
991
+ const [error, setError] = react.useState(null);
992
+ const refresh = react.useCallback(async () => {
993
+ if (!customer?.id) {
994
+ setItems([]);
995
+ return;
996
+ }
997
+ setLoading(true);
998
+ setError(null);
999
+ try {
1000
+ const data = await ctx.client.listWishlistItems(customer.id);
1001
+ setItems(data.data);
1002
+ } catch (err) {
1003
+ const e = err instanceof Error ? err : new Error(String(err));
1004
+ setError(e);
1005
+ setItems([]);
1006
+ } finally {
1007
+ setLoading(false);
1008
+ }
1009
+ }, [customer?.id, ctx.client]);
1010
+ react.useEffect(() => {
1011
+ refresh();
1012
+ }, [refresh]);
1013
+ const add = react.useCallback(async (productId) => {
1014
+ if (!customer?.id) throw new Error("Not authenticated");
1015
+ setError(null);
1016
+ try {
1017
+ const item = await ctx.client.addWishlistItem(customer.id, productId);
1018
+ setItems((prev) => [...prev, item]);
1019
+ return item;
1020
+ } catch (err) {
1021
+ const e = err instanceof Error ? err : new Error(String(err));
1022
+ setError(e);
1023
+ throw e;
1024
+ }
1025
+ }, [customer?.id, ctx.client]);
1026
+ const remove = react.useCallback(async (productId) => {
1027
+ if (!customer?.id) throw new Error("Not authenticated");
1028
+ setError(null);
1029
+ try {
1030
+ await ctx.client.removeWishlistItem(customer.id, productId);
1031
+ setItems((prev) => prev.filter((item) => item.product_id !== productId));
1032
+ } catch (err) {
1033
+ const e = err instanceof Error ? err : new Error(String(err));
1034
+ setError(e);
1035
+ throw e;
1036
+ }
1037
+ }, [customer?.id, ctx.client]);
1038
+ const has = react.useCallback((productId) => {
1039
+ return items.some((item) => item.product_id === productId);
1040
+ }, [items]);
1041
+ const toggle = react.useCallback(async (productId) => {
1042
+ if (has(productId)) {
1043
+ await remove(productId);
1044
+ } else {
1045
+ await add(productId);
1046
+ }
1047
+ }, [has, add, remove]);
1048
+ return { items, loading, error, refresh, add, remove, has, toggle };
1049
+ }
1050
+ function useRecommendations(params) {
1051
+ const ctx = react.useContext(WhaleContext);
1052
+ if (!ctx) throw new Error("useRecommendations must be used within <WhaleProvider>");
1053
+ const [recommendations, setRecommendations] = react.useState([]);
1054
+ const [loading, setLoading] = react.useState(false);
1055
+ const [error, setError] = react.useState(null);
1056
+ const productId = params?.product_id;
1057
+ const customerId = params?.customer_id;
1058
+ const limit = params?.limit;
1059
+ const type = params?.type;
1060
+ const refresh = react.useCallback(async () => {
1061
+ setLoading(true);
1062
+ setError(null);
1063
+ try {
1064
+ const data = await ctx.client.getRecommendations({
1065
+ product_id: productId,
1066
+ customer_id: customerId,
1067
+ limit,
1068
+ type
1069
+ });
1070
+ setRecommendations(data.data);
1071
+ } catch (err) {
1072
+ const e = err instanceof Error ? err : new Error(String(err));
1073
+ setError(e);
1074
+ setRecommendations([]);
1075
+ } finally {
1076
+ setLoading(false);
1077
+ }
1078
+ }, [ctx.client, productId, customerId, limit, type]);
1079
+ react.useEffect(() => {
1080
+ refresh();
1081
+ }, [refresh]);
1082
+ return { recommendations, loading, error, refresh };
1083
+ }
1084
+ function useLocations() {
1085
+ const ctx = react.useContext(WhaleContext);
1086
+ if (!ctx) throw new Error("useLocations must be used within <WhaleProvider>");
1087
+ const [locations, setLocations] = react.useState([]);
1088
+ const [loading, setLoading] = react.useState(true);
1089
+ const [error, setError] = react.useState(null);
1090
+ const refresh = react.useCallback(async () => {
1091
+ setLoading(true);
1092
+ setError(null);
1093
+ try {
1094
+ const data = await ctx.client.listLocations();
1095
+ setLocations(data.data);
1096
+ } catch (err) {
1097
+ const e = err instanceof Error ? err : new Error(String(err));
1098
+ setError(e);
1099
+ setLocations([]);
1100
+ } finally {
1101
+ setLoading(false);
1102
+ }
1103
+ }, [ctx.client]);
1104
+ react.useEffect(() => {
1105
+ refresh();
1106
+ }, [refresh]);
1107
+ const getLocation = react.useCallback(async (id) => {
1108
+ return ctx.client.getLocation(id);
1109
+ }, [ctx.client]);
1110
+ return { locations, loading, error, refresh, getLocation };
1111
+ }
1112
+ function useShipping() {
1113
+ const ctx = react.useContext(WhaleContext);
1114
+ if (!ctx) throw new Error("useShipping must be used within <WhaleProvider>");
1115
+ const [methods, setMethods] = react.useState([]);
1116
+ const [rates, setRates] = react.useState([]);
1117
+ const [loading, setLoading] = react.useState(false);
1118
+ const [error, setError] = react.useState(null);
1119
+ const refreshMethods = react.useCallback(async () => {
1120
+ setLoading(true);
1121
+ setError(null);
1122
+ try {
1123
+ const data = await ctx.client.listShippingMethods();
1124
+ setMethods(data.data);
1125
+ } catch (err) {
1126
+ const e = err instanceof Error ? err : new Error(String(err));
1127
+ setError(e);
1128
+ setMethods([]);
1129
+ } finally {
1130
+ setLoading(false);
1131
+ }
1132
+ }, [ctx.client]);
1133
+ react.useEffect(() => {
1134
+ refreshMethods();
1135
+ }, [refreshMethods]);
1136
+ const calculateRates = react.useCallback(async (params) => {
1137
+ setLoading(true);
1138
+ setError(null);
1139
+ try {
1140
+ const data = await ctx.client.calculateShippingRates(params);
1141
+ setRates(data.data);
1142
+ return data.data;
1143
+ } catch (err) {
1144
+ const e = err instanceof Error ? err : new Error(String(err));
1145
+ setError(e);
1146
+ setRates([]);
1147
+ throw e;
1148
+ } finally {
1149
+ setLoading(false);
1150
+ }
1151
+ }, [ctx.client]);
1152
+ return { methods, rates, loading, error, refreshMethods, calculateRates };
1153
+ }
1154
+ function useCoupons() {
1155
+ const ctx = react.useContext(WhaleContext);
1156
+ if (!ctx) throw new Error("useCoupons must be used within <WhaleProvider>");
1157
+ const [validation, setValidation] = react.useState(null);
1158
+ const [loading, setLoading] = react.useState(false);
1159
+ const [error, setError] = react.useState(null);
1160
+ const validate = react.useCallback(async (code, cartId) => {
1161
+ setLoading(true);
1162
+ setError(null);
1163
+ try {
1164
+ const result = await ctx.client.validateCoupon(code, cartId ? { cart_id: cartId } : void 0);
1165
+ setValidation(result);
1166
+ return result;
1167
+ } catch (err) {
1168
+ const e = err instanceof Error ? err : new Error(String(err));
1169
+ setError(e);
1170
+ setValidation(null);
1171
+ throw e;
1172
+ } finally {
1173
+ setLoading(false);
1174
+ }
1175
+ }, [ctx.client]);
1176
+ const apply = react.useCallback(async (cartId, code) => {
1177
+ setLoading(true);
1178
+ setError(null);
1179
+ try {
1180
+ const cart = await ctx.client.applyCoupon(cartId, code);
1181
+ return cart;
1182
+ } catch (err) {
1183
+ const e = err instanceof Error ? err : new Error(String(err));
1184
+ setError(e);
1185
+ throw e;
1186
+ } finally {
1187
+ setLoading(false);
1188
+ }
1189
+ }, [ctx.client]);
1190
+ const remove = react.useCallback(async (cartId) => {
1191
+ setLoading(true);
1192
+ setError(null);
1193
+ try {
1194
+ const cart = await ctx.client.removeCoupon(cartId);
1195
+ setValidation(null);
1196
+ return cart;
1197
+ } catch (err) {
1198
+ const e = err instanceof Error ? err : new Error(String(err));
1199
+ setError(e);
1200
+ throw e;
1201
+ } finally {
1202
+ setLoading(false);
1203
+ }
1204
+ }, [ctx.client]);
1205
+ const clear = react.useCallback(() => {
1206
+ setValidation(null);
1207
+ setError(null);
1208
+ }, []);
1209
+ return { validation, loading, error, validate, apply, remove, clear };
1210
+ }
1211
+ function QRLandingPage({
1212
+ code,
1213
+ gatewayUrl = "https://whale-gateway.fly.dev",
1214
+ renderProduct,
1215
+ renderCOA,
1216
+ renderPage,
1217
+ onDataLoaded,
1218
+ onError
1219
+ }) {
1220
+ const [state, setState] = react.useState("loading");
1221
+ const [data, setData] = react.useState(null);
1222
+ const [errorMsg, setErrorMsg] = react.useState("");
1223
+ react.useEffect(() => {
1224
+ if (!code) return;
1225
+ let cancelled = false;
1226
+ async function load() {
1227
+ try {
1228
+ const res = await fetch(`${gatewayUrl}/q/${encodeURIComponent(code)}/page`);
1229
+ if (!cancelled) {
1230
+ if (res.status === 404) {
1231
+ setState("not_found");
1232
+ return;
1233
+ }
1234
+ if (res.status === 410) {
1235
+ setState("expired");
1236
+ return;
1237
+ }
1238
+ if (!res.ok) {
1239
+ const body = await res.json().catch(() => ({}));
1240
+ throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1241
+ }
1242
+ const json = await res.json();
1243
+ setData(json);
1244
+ setState("ready");
1245
+ onDataLoaded?.(json);
1246
+ }
1247
+ } catch (err) {
1248
+ if (!cancelled) {
1249
+ const e = err instanceof Error ? err : new Error(String(err));
1250
+ setErrorMsg(e.message);
1251
+ setState("error");
1252
+ onError?.(e);
1253
+ }
1254
+ }
1255
+ }
1256
+ load();
1257
+ return () => {
1258
+ cancelled = true;
1259
+ };
1260
+ }, [code, gatewayUrl]);
1261
+ if (state === "loading") return /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {});
1262
+ if (state === "not_found") return /* @__PURE__ */ jsxRuntime.jsx(DefaultNotFound, {});
1263
+ if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired, {});
1264
+ if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError, { message: errorMsg });
1265
+ if (!data) return null;
1266
+ if (renderPage) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderPage(data) });
1267
+ return /* @__PURE__ */ jsxRuntime.jsx(DefaultLayout, { data, renderProduct, renderCOA });
1268
+ }
1269
+ function DefaultLayout({
1270
+ data,
1271
+ renderProduct,
1272
+ renderCOA
1273
+ }) {
1274
+ const { qr_code: qr, store, product, coa } = data;
1275
+ const lp = qr.landing_page;
1276
+ const [showCOA, setShowCOA] = react.useState(false);
1277
+ const bg = lp.background_color || store?.theme?.background || "#050505";
1278
+ const fg = lp.text_color || store?.theme?.foreground || "#fafafa";
1279
+ const accent = store?.theme?.accent || qr.brand_color || "#E8E2D9";
1280
+ const surface = store?.theme?.surface || "#111";
1281
+ const muted = store?.theme?.muted || "#888";
1282
+ const logoUrl = qr.logo_url || store?.logo_url;
1283
+ const productImage = lp.image_url || product?.featured_image || product?.image_gallery?.[0] || null;
1284
+ const productName = lp.title || product?.name || qr.name;
1285
+ const description = lp.description || product?.description || "";
1286
+ const ctaText = lp.cta_text || (coa ? "View Lab Results" : "Learn More");
1287
+ const ctaUrl = lp.cta_url || qr.destination_url;
1288
+ const cf = product?.custom_fields;
1289
+ const thca = cf?.thca_percentage ?? null;
1290
+ const thc = cf?.d9_percentage ?? null;
1291
+ const cbd = cf?.cbd_total ?? null;
1292
+ const strainType = cf?.strain_type ?? null;
1293
+ const categoryName = product?.category_name ?? null;
1294
+ const handleCOAClick = react.useCallback(() => {
1295
+ if (coa) setShowCOA(true);
1296
+ }, [coa]);
1297
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: bg, color: fg, fontFamily: "system-ui, -apple-system, sans-serif" }, children: [
1298
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: logoUrl && /* @__PURE__ */ jsxRuntime.jsx(
1299
+ "img",
1300
+ {
1301
+ src: logoUrl,
1302
+ alt: store?.name || "Store",
1303
+ style: { height: 40, objectFit: "contain" }
1304
+ }
1305
+ ) }),
1306
+ productImage && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: "100%", aspectRatio: "1", overflow: "hidden", background: surface }, children: /* @__PURE__ */ jsxRuntime.jsx(
1307
+ "img",
1308
+ {
1309
+ src: productImage,
1310
+ alt: productName,
1311
+ style: { width: "100%", height: "100%", objectFit: "cover" }
1312
+ }
1313
+ ) }),
1314
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: [
1315
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", marginBottom: "0.75rem", flexWrap: "wrap" }, children: [
1316
+ strainType && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
1317
+ padding: "0.25rem 0.75rem",
1318
+ borderRadius: 999,
1319
+ fontSize: "0.75rem",
1320
+ fontWeight: 600,
1321
+ textTransform: "uppercase",
1322
+ letterSpacing: "0.05em",
1323
+ background: strainBadgeColor(strainType),
1324
+ color: "#fff"
1325
+ }, children: strainType }),
1326
+ categoryName && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
1327
+ padding: "0.25rem 0.75rem",
1328
+ borderRadius: 999,
1329
+ fontSize: "0.75rem",
1330
+ background: surface,
1331
+ color: muted
1332
+ }, children: categoryName })
1333
+ ] }),
1334
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.75rem", fontWeight: 600, margin: "0 0 0.5rem", lineHeight: 1.2 }, children: productName }),
1335
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: muted, lineHeight: 1.6, margin: "0 0 1.5rem", fontSize: "0.95rem" }, children: description }),
1336
+ renderProduct ? renderProduct(data) : null,
1337
+ (thca || thc || cbd) && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "0.75rem", margin: "1.5rem 0" }, children: [
1338
+ thca != null && /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "THCa", value: `${thca.toFixed(1)}%`, bg: surface, fg, accent }),
1339
+ thc != null && /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "D9 THC", value: `${thc.toFixed(1)}%`, bg: surface, fg, accent }),
1340
+ cbd != null && /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "CBD", value: `${cbd.toFixed(1)}%`, bg: surface, fg, accent })
1341
+ ] }),
1342
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem", marginTop: "1.5rem" }, children: [
1343
+ coa && /* @__PURE__ */ jsxRuntime.jsx(
1344
+ "button",
1345
+ {
1346
+ onClick: handleCOAClick,
1347
+ style: {
1348
+ width: "100%",
1349
+ padding: "0.875rem",
1350
+ background: accent,
1351
+ color: bg,
1352
+ border: "none",
1353
+ fontSize: "0.95rem",
1354
+ fontWeight: 600,
1355
+ cursor: "pointer",
1356
+ borderRadius: 0
1357
+ },
1358
+ children: "View Lab Results"
1359
+ }
1360
+ ),
1361
+ renderCOA ? renderCOA(data) : null,
1362
+ /* @__PURE__ */ jsxRuntime.jsx(
1363
+ "a",
1364
+ {
1365
+ href: ctaUrl,
1366
+ style: {
1367
+ display: "block",
1368
+ width: "100%",
1369
+ padding: "0.875rem",
1370
+ background: "transparent",
1371
+ color: fg,
1372
+ border: `1px solid ${muted}`,
1373
+ fontSize: "0.95rem",
1374
+ fontWeight: 500,
1375
+ textAlign: "center",
1376
+ textDecoration: "none",
1377
+ boxSizing: "border-box",
1378
+ borderRadius: 0
1379
+ },
1380
+ children: ctaText === "View Lab Results" ? "Shop Online" : ctaText
1381
+ }
1382
+ )
1383
+ ] }),
1384
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "2rem", paddingTop: "1.5rem", borderTop: `1px solid ${surface}`, textAlign: "center" }, children: [
1385
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "center", gap: "1.5rem", marginBottom: "0.75rem" }, children: [
1386
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.75rem", color: muted, textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Lab Tested" }),
1387
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.75rem", color: muted, textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Authentic" })
1388
+ ] }),
1389
+ store?.name && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: muted, margin: 0 }, children: [
1390
+ "Verified by ",
1391
+ store.name
1392
+ ] })
1393
+ ] })
1394
+ ] }),
1395
+ showCOA && coa && /* @__PURE__ */ jsxRuntime.jsxs(
1396
+ "div",
1397
+ {
1398
+ style: {
1399
+ position: "fixed",
1400
+ inset: 0,
1401
+ zIndex: 9999,
1402
+ background: "rgba(0,0,0,0.9)",
1403
+ display: "flex",
1404
+ flexDirection: "column"
1405
+ },
1406
+ children: [
1407
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "1rem" }, children: [
1408
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#fff", fontWeight: 600 }, children: coa.document_name || "Lab Results" }),
1409
+ /* @__PURE__ */ jsxRuntime.jsx(
1410
+ "button",
1411
+ {
1412
+ onClick: () => setShowCOA(false),
1413
+ style: { background: "none", border: "none", color: "#fff", fontSize: "1.5rem", cursor: "pointer", padding: "0.5rem" },
1414
+ children: "\u2715"
1415
+ }
1416
+ )
1417
+ ] }),
1418
+ /* @__PURE__ */ jsxRuntime.jsx(
1419
+ "iframe",
1420
+ {
1421
+ src: coa.url,
1422
+ style: { flex: 1, border: "none", background: "#fff" },
1423
+ title: "Lab Results"
1424
+ }
1425
+ )
1426
+ ]
1427
+ }
1428
+ )
1429
+ ] });
1430
+ }
1431
+ function StatCard({ label, value, bg, fg, accent }) {
1432
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { background: bg, padding: "1rem", textAlign: "center" }, children: [
1433
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "1.25rem", fontWeight: 700, color: fg }, children: value }),
1434
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "0.7rem", color: accent, textTransform: "uppercase", letterSpacing: "0.05em", marginTop: "0.25rem" }, children: label })
1435
+ ] });
1436
+ }
1437
+ function strainBadgeColor(strain) {
1438
+ const s = strain.toLowerCase();
1439
+ if (s === "sativa") return "#22c55e";
1440
+ if (s === "indica") return "#8b5cf6";
1441
+ if (s === "hybrid") return "#f59e0b";
1442
+ return "#6b7280";
1443
+ }
1444
+ var containerStyle = {
1445
+ minHeight: "100dvh",
1446
+ display: "flex",
1447
+ justifyContent: "center",
1448
+ alignItems: "center",
1449
+ fontFamily: "system-ui, -apple-system, sans-serif",
1450
+ background: "#050505",
1451
+ color: "#fafafa",
1452
+ textAlign: "center",
1453
+ padding: "2rem"
1454
+ };
1455
+ function DefaultLoading() {
1456
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1457
+ /* @__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" } }),
1458
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1459
+ ] }) });
1460
+ }
1461
+ function DefaultNotFound() {
1462
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1463
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "QR Code Not Found" }),
1464
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This QR code does not exist or has been deactivated." })
1465
+ ] }) });
1466
+ }
1467
+ function DefaultExpired() {
1468
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1469
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "QR Code Expired" }),
1470
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This QR code is no longer active." })
1471
+ ] }) });
1472
+ }
1473
+ function DefaultError({ message }) {
1474
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1475
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Something Went Wrong" }),
1476
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: message || "Please try scanning again." })
1477
+ ] }) });
1478
+ }
1479
+ function LandingPage({
1480
+ slug,
1481
+ gatewayUrl = "https://whale-gateway.fly.dev",
1482
+ renderSection,
1483
+ onDataLoaded,
1484
+ onError
1485
+ }) {
1486
+ const [state, setState] = react.useState("loading");
1487
+ const [data, setData] = react.useState(null);
1488
+ const [errorMsg, setErrorMsg] = react.useState("");
1489
+ react.useEffect(() => {
1490
+ if (!slug) return;
1491
+ let cancelled = false;
1492
+ async function load() {
1493
+ try {
1494
+ const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
1495
+ if (!cancelled) {
1496
+ if (res.status === 404) {
1497
+ setState("not_found");
1498
+ return;
1499
+ }
1500
+ if (res.status === 410) {
1501
+ setState("expired");
1502
+ return;
1503
+ }
1504
+ if (!res.ok) {
1505
+ const body = await res.json().catch(() => ({}));
1506
+ throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1507
+ }
1508
+ const json = await res.json();
1509
+ setData(json);
1510
+ setState("ready");
1511
+ onDataLoaded?.(json);
1512
+ }
1513
+ } catch (err) {
1514
+ if (!cancelled) {
1515
+ const e = err instanceof Error ? err : new Error(String(err));
1516
+ setErrorMsg(e.message);
1517
+ setState("error");
1518
+ onError?.(e);
1519
+ }
1520
+ }
1521
+ }
1522
+ load();
1523
+ return () => {
1524
+ cancelled = true;
1525
+ };
1526
+ }, [slug, gatewayUrl]);
1527
+ if (state === "loading") return /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading2, {});
1528
+ if (state === "not_found") return /* @__PURE__ */ jsxRuntime.jsx(DefaultNotFound2, {});
1529
+ if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired2, {});
1530
+ if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError2, { message: errorMsg });
1531
+ if (!data) return null;
1532
+ return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, renderSection });
1533
+ }
1534
+ function PageLayout({
1535
+ data,
1536
+ renderSection
1537
+ }) {
1538
+ const { landing_page: lp, store } = data;
1539
+ const [showCOA, setShowCOA] = react.useState(false);
1540
+ const theme = {
1541
+ bg: lp.background_color || store?.theme?.background || "#050505",
1542
+ fg: lp.text_color || store?.theme?.foreground || "#fafafa",
1543
+ accent: lp.accent_color || store?.theme?.accent || "#E8E2D9",
1544
+ surface: store?.theme?.surface || "#111",
1545
+ muted: store?.theme?.muted || "#888"
1546
+ };
1547
+ const fontFamily = lp.font_family || "system-ui, -apple-system, sans-serif";
1548
+ const logoUrl = store?.logo_url;
1549
+ const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
1550
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1551
+ lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
1552
+ logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1553
+ "img",
1554
+ {
1555
+ src: logoUrl,
1556
+ alt: store?.name || "Store",
1557
+ style: { height: 40, objectFit: "contain" }
1558
+ }
1559
+ ) }),
1560
+ sorted.map((section) => {
1561
+ const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(
1562
+ DefaultSectionRenderer,
1563
+ {
1564
+ section,
1565
+ data,
1566
+ theme,
1567
+ onShowCOA: () => setShowCOA(true)
1568
+ },
1569
+ section.id
1570
+ );
1571
+ if (renderSection) {
1572
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1573
+ }
1574
+ return /* @__PURE__ */ jsxRuntime.jsx(
1575
+ DefaultSectionRenderer,
1576
+ {
1577
+ section,
1578
+ data,
1579
+ theme,
1580
+ onShowCOA: () => setShowCOA(true)
1581
+ },
1582
+ section.id
1583
+ );
1584
+ }),
1585
+ 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: [
1586
+ "Powered by ",
1587
+ store.name
1588
+ ] }) }),
1589
+ showCOA && data.coa && /* @__PURE__ */ jsxRuntime.jsxs(
1590
+ "div",
1591
+ {
1592
+ style: {
1593
+ position: "fixed",
1594
+ inset: 0,
1595
+ zIndex: 9999,
1596
+ background: "rgba(0,0,0,0.9)",
1597
+ display: "flex",
1598
+ flexDirection: "column"
1599
+ },
1600
+ children: [
1601
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "1rem" }, children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#fff", fontWeight: 600 }, children: data.coa.document_name || "Lab Results" }),
1603
+ /* @__PURE__ */ jsxRuntime.jsx(
1604
+ "button",
1605
+ {
1606
+ onClick: () => setShowCOA(false),
1607
+ style: { background: "none", border: "none", color: "#fff", fontSize: "1.5rem", cursor: "pointer", padding: "0.5rem" },
1608
+ children: "\u2715"
1609
+ }
1610
+ )
1611
+ ] }),
1612
+ /* @__PURE__ */ jsxRuntime.jsx(
1613
+ "iframe",
1614
+ {
1615
+ src: data.coa.url,
1616
+ style: { flex: 1, border: "none", background: "#fff" },
1617
+ title: "Lab Results"
1618
+ }
1619
+ )
1620
+ ]
1621
+ }
1622
+ )
1623
+ ] });
1624
+ }
1625
+ function DefaultSectionRenderer({
1626
+ section,
1627
+ data,
1628
+ theme,
1629
+ onShowCOA
1630
+ }) {
1631
+ switch (section.type) {
1632
+ case "hero":
1633
+ return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme });
1634
+ case "text":
1635
+ return /* @__PURE__ */ jsxRuntime.jsx(TextSection, { section, theme });
1636
+ case "image":
1637
+ return /* @__PURE__ */ jsxRuntime.jsx(ImageSection, { section, theme });
1638
+ case "video":
1639
+ return /* @__PURE__ */ jsxRuntime.jsx(VideoSection, { section, theme });
1640
+ case "gallery":
1641
+ return /* @__PURE__ */ jsxRuntime.jsx(GallerySection, { section, theme });
1642
+ case "cta":
1643
+ return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme });
1644
+ case "stats":
1645
+ return /* @__PURE__ */ jsxRuntime.jsx(StatsSection, { section, theme });
1646
+ case "product_card":
1647
+ return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme });
1648
+ case "coa_viewer":
1649
+ return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA });
1650
+ case "social_links":
1651
+ return /* @__PURE__ */ jsxRuntime.jsx(SocialLinksSection, { section, theme });
1652
+ case "divider":
1653
+ return /* @__PURE__ */ jsxRuntime.jsx(DividerSection, { theme });
1654
+ default:
1655
+ return null;
1656
+ }
1657
+ }
1658
+ function HeroSection({ section, theme }) {
1659
+ const { title, subtitle, background_image, cta_text, cta_url } = section.content;
1660
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1661
+ "div",
1662
+ {
1663
+ style: {
1664
+ position: "relative",
1665
+ minHeight: "60vh",
1666
+ display: "flex",
1667
+ flexDirection: "column",
1668
+ justifyContent: "center",
1669
+ alignItems: "center",
1670
+ textAlign: "center",
1671
+ padding: "3rem 1.5rem",
1672
+ backgroundImage: background_image ? `url(${background_image})` : void 0,
1673
+ backgroundSize: "cover",
1674
+ backgroundPosition: "center"
1675
+ },
1676
+ children: [
1677
+ background_image && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" } }),
1678
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", zIndex: 1, maxWidth: 640 }, children: [
1679
+ title && /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "2.5rem", fontWeight: 700, margin: "0 0 1rem", lineHeight: 1.15, color: theme.fg }, children: title }),
1680
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "1.125rem", color: theme.muted, margin: "0 0 2rem", lineHeight: 1.6 }, children: subtitle }),
1681
+ cta_text && cta_url && /* @__PURE__ */ jsxRuntime.jsx(
1682
+ "a",
1683
+ {
1684
+ href: cta_url,
1685
+ style: {
1686
+ display: "inline-block",
1687
+ padding: "0.875rem 2rem",
1688
+ background: theme.accent,
1689
+ color: theme.bg,
1690
+ textDecoration: "none",
1691
+ fontSize: "0.95rem",
1692
+ fontWeight: 600,
1693
+ borderRadius: 0
1694
+ },
1695
+ children: cta_text
1696
+ }
1697
+ )
1698
+ ] })
1699
+ ]
1700
+ }
1701
+ );
1702
+ }
1703
+ function TextSection({ section, theme }) {
1704
+ const { heading, body } = section.content;
1705
+ const align = section.config?.align || "left";
1706
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 640, margin: "0 auto", textAlign: align }, children: [
1707
+ heading && /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontSize: "1.5rem", fontWeight: 600, margin: "0 0 1rem", color: theme.fg }, children: heading }),
1708
+ body && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: theme.muted, lineHeight: 1.7, fontSize: "0.95rem", whiteSpace: "pre-wrap" }, children: body })
1709
+ ] });
1710
+ }
1711
+ function ImageSection({ section, theme }) {
1712
+ const { url, alt, caption } = section.content;
1713
+ const contained = section.config?.contained !== false;
1714
+ if (!url) return null;
1715
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: contained ? "1.5rem" : 0, maxWidth: contained ? 640 : void 0, margin: contained ? "0 auto" : void 0 }, children: [
1716
+ /* @__PURE__ */ jsxRuntime.jsx(
1717
+ "img",
1718
+ {
1719
+ src: url,
1720
+ alt: alt || "",
1721
+ style: { width: "100%", display: "block", objectFit: "cover" }
1722
+ }
1723
+ ),
1724
+ caption && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: theme.muted, textAlign: "center", marginTop: "0.75rem" }, children: caption })
1725
+ ] });
1726
+ }
1727
+ function VideoSection({ section, theme }) {
1728
+ const { url, poster } = section.content;
1729
+ if (!url) return null;
1730
+ const isEmbed = url.includes("youtube") || url.includes("youtu.be") || url.includes("vimeo");
1731
+ 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(
1732
+ "iframe",
1733
+ {
1734
+ src: toEmbedUrl(url),
1735
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: "none" },
1736
+ allow: "autoplay; fullscreen",
1737
+ title: "Video"
1738
+ }
1739
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(
1740
+ "video",
1741
+ {
1742
+ src: url,
1743
+ poster,
1744
+ controls: true,
1745
+ style: { width: "100%", display: "block", background: theme.surface }
1746
+ }
1747
+ ) });
1748
+ }
1749
+ function GallerySection({ section, theme }) {
1750
+ const { images } = section.content;
1751
+ const columns = section.config?.columns || 3;
1752
+ if (!images || images.length === 0) return null;
1753
+ 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(
1754
+ "img",
1755
+ {
1756
+ src: img.url,
1757
+ alt: img.alt || "",
1758
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
1759
+ }
1760
+ ) }, i)) }) });
1761
+ }
1762
+ function CTASection({ section, theme }) {
1763
+ const { buttons } = section.content;
1764
+ if (!buttons || buttons.length === 0) return null;
1765
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: buttons.map((btn, i) => {
1766
+ const isPrimary = btn.style !== "outline";
1767
+ return /* @__PURE__ */ jsxRuntime.jsx(
1768
+ "a",
1769
+ {
1770
+ href: btn.url,
1771
+ style: {
1772
+ display: "block",
1773
+ width: "100%",
1774
+ padding: "0.875rem",
1775
+ background: isPrimary ? theme.accent : "transparent",
1776
+ color: isPrimary ? theme.bg : theme.fg,
1777
+ border: isPrimary ? "none" : `1px solid ${theme.muted}`,
1778
+ fontSize: "0.95rem",
1779
+ fontWeight: 600,
1780
+ textAlign: "center",
1781
+ textDecoration: "none",
1782
+ boxSizing: "border-box",
1783
+ borderRadius: 0
1784
+ },
1785
+ children: btn.text
1786
+ },
1787
+ i
1788
+ );
1789
+ }) });
1790
+ }
1791
+ function StatsSection({ section, theme }) {
1792
+ const { stats } = section.content;
1793
+ if (!stats || stats.length === 0) return null;
1794
+ const columns = Math.min(stats.length, 4);
1795
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: "0.75rem" }, children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { background: theme.surface, padding: "1rem", textAlign: "center" }, children: [
1796
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "1.25rem", fontWeight: 700, color: theme.fg }, children: stat.value }),
1797
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "0.7rem", color: theme.accent, textTransform: "uppercase", letterSpacing: "0.05em", marginTop: "0.25rem" }, children: stat.label })
1798
+ ] }, i)) }) });
1799
+ }
1800
+ function ProductCardSection({ section, data, theme }) {
1801
+ const product = data.product;
1802
+ const c = section.content;
1803
+ const name = c.name || product?.name || "";
1804
+ const description = c.description || product?.description || "";
1805
+ const price = c.price || null;
1806
+ const imageUrl = c.image_url || product?.featured_image || null;
1807
+ const url = c.url || null;
1808
+ 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: [
1809
+ 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" } }) }),
1810
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "1.25rem" }, children: [
1811
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem", color: theme.fg }, children: name }),
1812
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.9rem", color: theme.muted, margin: "0 0 1rem", lineHeight: 1.5 }, children: description }),
1813
+ price && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "1.125rem", fontWeight: 700, color: theme.accent, margin: "0 0 1rem" }, children: price }),
1814
+ url && /* @__PURE__ */ jsxRuntime.jsx(
1815
+ "a",
1816
+ {
1817
+ href: url,
1818
+ style: {
1819
+ display: "block",
1820
+ width: "100%",
1821
+ padding: "0.75rem",
1822
+ background: theme.accent,
1823
+ color: theme.bg,
1824
+ textAlign: "center",
1825
+ textDecoration: "none",
1826
+ fontSize: "0.9rem",
1827
+ fontWeight: 600,
1828
+ boxSizing: "border-box",
1829
+ borderRadius: 0
1830
+ },
1831
+ children: "View Product"
1832
+ }
1833
+ )
1834
+ ] })
1835
+ ] }) });
1836
+ }
1837
+ function COAViewerSection({
1838
+ section,
1839
+ data,
1840
+ theme,
1841
+ onShowCOA
1842
+ }) {
1843
+ const coa = data.coa;
1844
+ const c = section.content;
1845
+ if (!coa) return null;
1846
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1847
+ "button",
1848
+ {
1849
+ onClick: onShowCOA,
1850
+ style: {
1851
+ width: "100%",
1852
+ padding: "0.875rem",
1853
+ background: theme.accent,
1854
+ color: theme.bg,
1855
+ border: "none",
1856
+ fontSize: "0.95rem",
1857
+ fontWeight: 600,
1858
+ cursor: "pointer",
1859
+ borderRadius: 0
1860
+ },
1861
+ children: c.button_text || "View Lab Results"
1862
+ }
1863
+ ) });
1864
+ }
1865
+ function SocialLinksSection({ section, theme }) {
1866
+ const { links } = section.content;
1867
+ if (!links || links.length === 0) return null;
1868
+ 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(
1869
+ "a",
1870
+ {
1871
+ href: link.url,
1872
+ target: "_blank",
1873
+ rel: "noopener noreferrer",
1874
+ style: {
1875
+ color: theme.muted,
1876
+ textDecoration: "none",
1877
+ fontSize: "0.85rem",
1878
+ fontWeight: 500,
1879
+ textTransform: "capitalize",
1880
+ letterSpacing: "0.03em",
1881
+ transition: "color 0.15s"
1882
+ },
1883
+ children: link.platform
1884
+ },
1885
+ i
1886
+ )) });
1887
+ }
1888
+ function DividerSection({ theme }) {
1889
+ 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.surface}`, margin: 0 } }) });
1890
+ }
1891
+ function toEmbedUrl(url) {
1892
+ const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
1893
+ if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
1894
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
1895
+ if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
1896
+ return url;
1897
+ }
1898
+ var containerStyle2 = {
1899
+ minHeight: "100dvh",
1900
+ display: "flex",
1901
+ justifyContent: "center",
1902
+ alignItems: "center",
1903
+ fontFamily: "system-ui, -apple-system, sans-serif",
1904
+ background: "#050505",
1905
+ color: "#fafafa",
1906
+ textAlign: "center",
1907
+ padding: "2rem"
1908
+ };
1909
+ function DefaultLoading2() {
1910
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle2, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1911
+ /* @__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" } }),
1912
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1913
+ ] }) });
1914
+ }
1915
+ function DefaultNotFound2() {
1916
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle2, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1917
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Not Found" }),
1918
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page does not exist or has been removed." })
1919
+ ] }) });
1920
+ }
1921
+ function DefaultExpired2() {
1922
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle2, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1923
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Expired" }),
1924
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page is no longer active." })
1925
+ ] }) });
1926
+ }
1927
+ function DefaultError2({ message }) {
1928
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle2, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1929
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Something Went Wrong" }),
1930
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: message || "Please try again later." })
1931
+ ] }) });
1932
+ }
717
1933
 
718
1934
  exports.AnalyticsTracker = AnalyticsTracker;
719
1935
  exports.AuthInitializer = AuthInitializer;
720
1936
  exports.CartInitializer = CartInitializer;
1937
+ exports.LandingPage = LandingPage;
721
1938
  exports.PixelInitializer = PixelInitializer;
1939
+ exports.QRLandingPage = QRLandingPage;
722
1940
  exports.WhaleContext = WhaleContext;
723
1941
  exports.WhaleProvider = WhaleProvider;
724
1942
  exports.useAnalytics = useAnalytics;
@@ -726,10 +1944,20 @@ exports.useAuth = useAuth;
726
1944
  exports.useCart = useCart;
727
1945
  exports.useCartItemCount = useCartItemCount;
728
1946
  exports.useCartTotal = useCartTotal;
1947
+ exports.useCategories = useCategories;
1948
+ exports.useCheckout = useCheckout;
1949
+ exports.useCoupons = useCoupons;
729
1950
  exports.useCustomerAnalytics = useCustomerAnalytics;
730
1951
  exports.useCustomerOrders = useCustomerOrders;
1952
+ exports.useLocations = useLocations;
1953
+ exports.useLoyalty = useLoyalty;
731
1954
  exports.useProduct = useProduct;
732
1955
  exports.useProducts = useProducts;
1956
+ exports.useRecommendations = useRecommendations;
1957
+ exports.useReviews = useReviews;
1958
+ exports.useSearch = useSearch;
1959
+ exports.useShipping = useShipping;
733
1960
  exports.useWhaleClient = useWhaleClient;
1961
+ exports.useWishlist = useWishlist;
734
1962
  //# sourceMappingURL=index.cjs.map
735
1963
  //# sourceMappingURL=index.cjs.map