@neowhale/storefront 0.2.5 → 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.
- package/dist/{chunk-PVO5ZGBN.cjs → chunk-3VKRKDPL.cjs} +167 -2
- package/dist/chunk-3VKRKDPL.cjs.map +1 -0
- package/dist/{chunk-NXNLD7YI.js → chunk-M2MR6C55.js} +167 -2
- package/dist/chunk-M2MR6C55.js.map +1 -0
- package/dist/client-KHvDGThs.d.cts +567 -0
- package/dist/client-KHvDGThs.d.ts +567 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/next/index.cjs +4 -4
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +1 -1
- package/dist/{pixel-manager-Db6Czwr2.d.cts → pixel-manager-CQaginxZ.d.cts} +1 -1
- package/dist/{pixel-manager-CYtRIGo0.d.ts → pixel-manager-Djp4Mh9L.d.ts} +1 -1
- package/dist/react/index.cjs +1240 -12
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +207 -8
- package/dist/react/index.d.ts +207 -8
- package/dist/react/index.js +1229 -13
- package/dist/react/index.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-NXNLD7YI.js.map +0 -1
- package/dist/chunk-PVO5ZGBN.cjs.map +0 -1
- package/dist/client-y0V1x0px.d.cts +0 -305
- package/dist/client-y0V1x0px.d.ts +0 -305
package/dist/react/index.cjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkBTGOSNMP_cjs = require('../chunk-BTGOSNMP.cjs');
|
|
4
|
-
var
|
|
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 (
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
|
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
|
-
|
|
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
|