@godaddy/gd-commerce-storefront 0.2.0 → 0.4.0
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/README.md +118 -3
- package/dist/index.d.ts +225 -7
- package/dist/index.js +825 -326
- package/dist/styles.css +69 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
2
|
-
import { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
4
4
|
import { useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -122,8 +122,9 @@ function getAvailableInventoryQuantity(item) {
|
|
|
122
122
|
if (!edges || edges.length === 0) return null;
|
|
123
123
|
return edges.find((edge) => edge?.node?.type === "AVAILABLE")?.node?.quantity ?? 0;
|
|
124
124
|
}
|
|
125
|
+
const imageMediaTypes = new Set(["IMAGE", "PHOTO"]);
|
|
125
126
|
function getImageUrls(item) {
|
|
126
|
-
return item?.mediaObjects?.edges?.filter((edge) => edge?.node?.type
|
|
127
|
+
return item?.mediaObjects?.edges?.filter((edge) => imageMediaTypes.has(edge?.node?.type ?? "") && edge?.node?.url).map((edge) => edge?.node?.url).filter((url) => Boolean(url)) ?? [];
|
|
127
128
|
}
|
|
128
129
|
function getPrimaryImageUrl(item) {
|
|
129
130
|
return getImageUrls(item)[0] ?? null;
|
|
@@ -147,6 +148,10 @@ function getProductAttributes(skuGroup) {
|
|
|
147
148
|
};
|
|
148
149
|
}) ?? [];
|
|
149
150
|
}
|
|
151
|
+
/** The product's display name: its label, then its name. */
|
|
152
|
+
function getProductName(skuGroup) {
|
|
153
|
+
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
154
|
+
}
|
|
150
155
|
|
|
151
156
|
//#endregion
|
|
152
157
|
//#region src/sample-cart.tsx
|
|
@@ -651,9 +656,9 @@ function CartDrawer() {
|
|
|
651
656
|
subtotal: item.totals?.subTotal?.value,
|
|
652
657
|
currency: item.totals?.subTotal?.currencyCode
|
|
653
658
|
}));
|
|
654
|
-
const summary = getCartSummaryTotals(cart);
|
|
659
|
+
const summary$1 = getCartSummaryTotals(cart);
|
|
655
660
|
const currency = isSample ? "USD" : cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
656
|
-
const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary.subtotal;
|
|
661
|
+
const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary$1.subtotal;
|
|
657
662
|
async function handleAction(action, operation) {
|
|
658
663
|
if (locked) return;
|
|
659
664
|
setDrawerAction(action);
|
|
@@ -849,7 +854,35 @@ function CartDrawer() {
|
|
|
849
854
|
}
|
|
850
855
|
|
|
851
856
|
//#endregion
|
|
852
|
-
//#region src/
|
|
857
|
+
//#region src/part.ts
|
|
858
|
+
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
859
|
+
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
860
|
+
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
861
|
+
/**
|
|
862
|
+
* Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
|
|
863
|
+
* children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
|
|
864
|
+
*/
|
|
865
|
+
function usePartRegistry(onChange) {
|
|
866
|
+
const counts = useRef(/* @__PURE__ */ new Map());
|
|
867
|
+
const has = useCallback((key) => counts.current.has(key), []);
|
|
868
|
+
const register = useCallback((key) => {
|
|
869
|
+
counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
|
|
870
|
+
onChange?.(has);
|
|
871
|
+
return () => {
|
|
872
|
+
const count = (counts.current.get(key) ?? 1) - 1;
|
|
873
|
+
if (count > 0) counts.current.set(key, count);
|
|
874
|
+
else counts.current.delete(key);
|
|
875
|
+
onChange?.(has);
|
|
876
|
+
};
|
|
877
|
+
}, [has, onChange]);
|
|
878
|
+
return useMemo(() => ({
|
|
879
|
+
register,
|
|
880
|
+
has
|
|
881
|
+
}), [register, has]);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/product-card.tsx
|
|
853
886
|
function ProductImage({ url, name, className = "" }) {
|
|
854
887
|
const [failed, setFailed] = useState(false);
|
|
855
888
|
return url && !failed ? /* @__PURE__ */ jsx("img", {
|
|
@@ -865,74 +898,257 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
865
898
|
children: "No image available"
|
|
866
899
|
});
|
|
867
900
|
}
|
|
868
|
-
|
|
869
|
-
|
|
901
|
+
/** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
|
|
902
|
+
const CatalogItemContext = createContext(null);
|
|
903
|
+
const titleLink = "title-link";
|
|
904
|
+
function cardValue(item, productPath, links) {
|
|
905
|
+
if (item.source === "sample") return {
|
|
906
|
+
...links,
|
|
907
|
+
source: "sample",
|
|
908
|
+
sample: item.sample,
|
|
909
|
+
product: null,
|
|
910
|
+
name: item.sample.name,
|
|
911
|
+
href: null,
|
|
912
|
+
isSample: true
|
|
913
|
+
};
|
|
914
|
+
return {
|
|
915
|
+
...links,
|
|
916
|
+
source: "live",
|
|
917
|
+
product: item.product,
|
|
918
|
+
name: getProductName(item.product),
|
|
919
|
+
href: `${productPath}/${encodeURIComponent(item.product.id ?? "")}`,
|
|
920
|
+
isSample: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const ProductCardContext = createContext(null);
|
|
924
|
+
function useProductCardInternal(part) {
|
|
925
|
+
const context = useContext(ProductCardContext);
|
|
926
|
+
if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
|
|
927
|
+
return context;
|
|
928
|
+
}
|
|
929
|
+
/** Read the card's product, name, and link from inside `ProductCardRoot`. */
|
|
930
|
+
function useProductCard() {
|
|
931
|
+
const { product, name, href, isSample } = useProductCardInternal("useProductCard");
|
|
932
|
+
return useMemo(() => ({
|
|
933
|
+
product,
|
|
934
|
+
name,
|
|
935
|
+
href,
|
|
936
|
+
isSample
|
|
937
|
+
}), [
|
|
938
|
+
product,
|
|
939
|
+
name,
|
|
940
|
+
href,
|
|
941
|
+
isSample
|
|
942
|
+
]);
|
|
870
943
|
}
|
|
871
|
-
|
|
944
|
+
/** Provides one product to card parts. Inside `CatalogProducts` it renders the current catalog entry. */
|
|
945
|
+
function ProductCardRoot({ children, product, className }) {
|
|
946
|
+
const catalogItem = useContext(CatalogItemContext);
|
|
872
947
|
const { config } = useCommerce();
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
948
|
+
const [imageLinked, setImageLinked] = useState(false);
|
|
949
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
|
|
950
|
+
useEffect(() => {
|
|
951
|
+
setImageLinked(!has(titleLink));
|
|
952
|
+
}, [has]);
|
|
953
|
+
const value = useMemo(() => {
|
|
954
|
+
const item = catalogItem ?? (product ? {
|
|
955
|
+
source: "live",
|
|
956
|
+
product
|
|
957
|
+
} : null);
|
|
958
|
+
return item && cardValue(item, config.productPath, {
|
|
959
|
+
imageLinked,
|
|
960
|
+
register
|
|
961
|
+
});
|
|
962
|
+
}, [
|
|
963
|
+
catalogItem,
|
|
964
|
+
product,
|
|
965
|
+
config.productPath,
|
|
966
|
+
imageLinked,
|
|
967
|
+
register
|
|
968
|
+
]);
|
|
969
|
+
if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
|
|
970
|
+
if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
|
|
971
|
+
const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
|
|
972
|
+
value,
|
|
973
|
+
children: /* @__PURE__ */ jsx("article", {
|
|
974
|
+
className: partClass("group", className),
|
|
975
|
+
children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
976
|
+
value: null,
|
|
977
|
+
children
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
});
|
|
981
|
+
return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* The product's first image, linked to its page. Samples show a placeholder. With a `ProductCardTitle` in the card,
|
|
985
|
+
* the title is the one link keyboard and screen reader users get; without one, the image link is.
|
|
986
|
+
*/
|
|
987
|
+
function ProductCardImage({ className }) {
|
|
988
|
+
const card = useProductCardInternal("ProductCardImage");
|
|
989
|
+
return /* @__PURE__ */ jsx("div", {
|
|
990
|
+
className: partClass(className),
|
|
991
|
+
children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
|
|
992
|
+
to: card.href,
|
|
993
|
+
tabIndex: card.imageLinked ? void 0 : -1,
|
|
994
|
+
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
995
|
+
className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-4",
|
|
887
996
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
888
|
-
url: getPrimaryImageUrl(product),
|
|
889
|
-
name,
|
|
997
|
+
url: getPrimaryImageUrl(card.product),
|
|
998
|
+
name: card.name,
|
|
890
999
|
className: "motion-safe:transition-transform motion-safe:duration-200 motion-safe:group-hover:scale-105"
|
|
891
1000
|
})
|
|
892
|
-
})
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1001
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1002
|
+
"aria-hidden": "true",
|
|
1003
|
+
className: "flex aspect-square items-center justify-center bg-neutral-100",
|
|
1004
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
|
|
1005
|
+
})
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
/** The product name as an H2, linked to its page. */
|
|
1009
|
+
function ProductCardTitle({ className }) {
|
|
1010
|
+
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
1011
|
+
useEffect(() => href ? register(titleLink) : void 0, [href, register]);
|
|
1012
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1013
|
+
className: partClass(className),
|
|
1014
|
+
children: /* @__PURE__ */ jsx("h2", {
|
|
1015
|
+
className: "break-words text-2xl font-semibold leading-tight",
|
|
1016
|
+
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
1017
|
+
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
1018
|
+
to: href,
|
|
1019
|
+
children: name
|
|
1020
|
+
}) : name
|
|
1021
|
+
})
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
/** Up to three lines of the product description. Renders nothing without one. */
|
|
1025
|
+
function ProductCardDescription({ className }) {
|
|
1026
|
+
const { product } = useProductCardInternal("ProductCardDescription");
|
|
1027
|
+
if (!product?.description) return null;
|
|
1028
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1029
|
+
className: partClass(className),
|
|
1030
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1031
|
+
className: "line-clamp-3 text-sm leading-6",
|
|
1032
|
+
children: product.description
|
|
1033
|
+
})
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/** The product's price, or its price range when variants differ. */
|
|
1037
|
+
function ProductCardPrice({ className }) {
|
|
1038
|
+
const card = useProductCardInternal("ProductCardPrice");
|
|
1039
|
+
const { config } = useCommerce();
|
|
1040
|
+
let text;
|
|
1041
|
+
if (card.source === "sample") text = money(card.sample.price, "USD");
|
|
1042
|
+
else {
|
|
1043
|
+
const min = card.product.priceRange?.min;
|
|
1044
|
+
const max = card.product.priceRange?.max;
|
|
1045
|
+
text = typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable";
|
|
1046
|
+
}
|
|
1047
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1048
|
+
className: partClass(className),
|
|
1049
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1050
|
+
className: "text-lg font-semibold leading-7",
|
|
1051
|
+
children: text
|
|
1052
|
+
})
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1057
|
+
* Samples add to the sample cart.
|
|
1058
|
+
*/
|
|
1059
|
+
function ProductCardAction({ className }) {
|
|
1060
|
+
const card = useProductCardInternal("ProductCardAction");
|
|
1061
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1062
|
+
className: partClass(className),
|
|
1063
|
+
children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
|
|
1064
|
+
product: card.product,
|
|
1065
|
+
name: card.name,
|
|
1066
|
+
href: card.href
|
|
1067
|
+
}) : /* @__PURE__ */ jsx(SampleAction, { sample: card.sample })
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function LiveAction({ product, name, href }) {
|
|
1071
|
+
const selectedSku = getSingleMatchedSku(product);
|
|
1072
|
+
const hasVariants = getProductAttributes(product).length > 0;
|
|
1073
|
+
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCartButton, {
|
|
1074
|
+
sku: selectedSku,
|
|
1075
|
+
name
|
|
1076
|
+
});
|
|
1077
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
1078
|
+
className: buttonClass,
|
|
1079
|
+
to: href,
|
|
1080
|
+
children: hasVariants ? "Choose options" : "View product"
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function SampleAction({ sample }) {
|
|
1084
|
+
const { add } = useSampleCart();
|
|
1085
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1086
|
+
type: "button",
|
|
1087
|
+
className: buttonClass,
|
|
1088
|
+
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1089
|
+
onClick: () => add(sample.key),
|
|
1090
|
+
children: "Add to cart"
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
const packagedCardClass = "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200";
|
|
1094
|
+
/** The packaged card's parts, inside a root that has `packagedCardClass`. */
|
|
1095
|
+
function PackagedCardParts() {
|
|
1096
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1097
|
+
className: "flex flex-1 flex-col p-6",
|
|
1098
|
+
children: [
|
|
1099
|
+
/* @__PURE__ */ jsx(ProductCardTitle, {}),
|
|
1100
|
+
/* @__PURE__ */ jsx(ProductCardDescription, { className: "mt-3" }),
|
|
1101
|
+
/* @__PURE__ */ jsx("div", {
|
|
1102
|
+
className: "mt-auto pt-6",
|
|
1103
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1104
|
+
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1105
|
+
children: [/* @__PURE__ */ jsx(ProductCardPrice, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ProductCardAction, { className: "max-w-full" })]
|
|
926
1106
|
})
|
|
927
|
-
|
|
928
|
-
|
|
1107
|
+
})
|
|
1108
|
+
]
|
|
1109
|
+
})] });
|
|
1110
|
+
}
|
|
1111
|
+
/** The packaged card for one `SKUGroup`. Compose `ProductCardRoot` and the card parts for a custom card. */
|
|
1112
|
+
function ProductCard({ product }) {
|
|
1113
|
+
return /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1114
|
+
product,
|
|
1115
|
+
className: packagedCardClass,
|
|
1116
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
929
1117
|
});
|
|
930
1118
|
}
|
|
931
|
-
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/catalog.tsx
|
|
1122
|
+
const CatalogContext = createContext(null);
|
|
1123
|
+
const sampleCatalog = { source: "sample" };
|
|
1124
|
+
function useCatalogInternal(part) {
|
|
1125
|
+
const context = useContext(CatalogContext);
|
|
1126
|
+
if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
|
|
1127
|
+
return context;
|
|
1128
|
+
}
|
|
1129
|
+
/** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
|
|
1130
|
+
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
|
|
932
1131
|
const { connection } = useCommerce();
|
|
933
|
-
|
|
1132
|
+
let content;
|
|
1133
|
+
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
1134
|
+
className,
|
|
1135
|
+
children
|
|
1136
|
+
});
|
|
1137
|
+
else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1138
|
+
value: sampleCatalog,
|
|
1139
|
+
children: /* @__PURE__ */ jsx("section", {
|
|
1140
|
+
className: partClass(className),
|
|
1141
|
+
"data-commerce-source": "sample",
|
|
1142
|
+
children
|
|
1143
|
+
})
|
|
1144
|
+
});
|
|
1145
|
+
else content = /* @__PURE__ */ jsx("div", {
|
|
1146
|
+
className: partClass(className),
|
|
1147
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1148
|
+
});
|
|
1149
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: content });
|
|
934
1150
|
}
|
|
935
|
-
function
|
|
1151
|
+
function CatalogLoader({ children, className }) {
|
|
936
1152
|
const { config } = useCommerce();
|
|
937
1153
|
const [params, setParams] = useSearchParams();
|
|
938
1154
|
const after = params.get("after") ?? "";
|
|
@@ -951,34 +1167,94 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
951
1167
|
headers: { "X-Commerce-Scope": config.cartScope }
|
|
952
1168
|
})
|
|
953
1169
|
});
|
|
954
|
-
|
|
1170
|
+
const goToPage = useCallback((cursor) => {
|
|
955
1171
|
const next = new URLSearchParams(params);
|
|
956
1172
|
if (cursor) next.set("after", cursor);
|
|
957
1173
|
else next.delete("after");
|
|
958
1174
|
setParams(next);
|
|
1175
|
+
}, [params, setParams]);
|
|
1176
|
+
const nodes = useMemo(() => products.data?.skuGroups?.edges?.flatMap((edge) => edge?.node?.id ? [edge.node] : []) ?? [], [products.data]);
|
|
1177
|
+
const value = useMemo(() => ({
|
|
1178
|
+
source: "live",
|
|
1179
|
+
products,
|
|
1180
|
+
nodes,
|
|
1181
|
+
after,
|
|
1182
|
+
goToPage
|
|
1183
|
+
}), [
|
|
1184
|
+
products,
|
|
1185
|
+
nodes,
|
|
1186
|
+
after,
|
|
1187
|
+
goToPage
|
|
1188
|
+
]);
|
|
1189
|
+
return /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1190
|
+
value,
|
|
1191
|
+
children: /* @__PURE__ */ jsx("section", {
|
|
1192
|
+
className: partClass(className),
|
|
1193
|
+
children
|
|
1194
|
+
})
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
/** The catalog title as the page's H1, with a description. */
|
|
1198
|
+
function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
|
|
1199
|
+
useCatalogInternal("CatalogHeader");
|
|
1200
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
1201
|
+
className: partClass(className),
|
|
1202
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1203
|
+
className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
|
|
1204
|
+
children: title
|
|
1205
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1206
|
+
className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
|
|
1207
|
+
children: description
|
|
1208
|
+
})]
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
/** Announces loading, failure, or how many products the page shows. */
|
|
1212
|
+
function CatalogSummary({ className }) {
|
|
1213
|
+
const catalog = useCatalogInternal("CatalogSummary");
|
|
1214
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1215
|
+
className: partClass(className),
|
|
1216
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1217
|
+
className: "text-sm",
|
|
1218
|
+
role: "status",
|
|
1219
|
+
children: summary(catalog)
|
|
1220
|
+
})
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function summary(catalog) {
|
|
1224
|
+
if (catalog.source === "sample") return "Sample products · Example prices in USD";
|
|
1225
|
+
if (catalog.products.isPending) return "Loading products…";
|
|
1226
|
+
if (catalog.products.isError) return "Products unavailable";
|
|
1227
|
+
return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
|
|
1228
|
+
}
|
|
1229
|
+
const sampleItems = sampleProducts.map((sample) => ({
|
|
1230
|
+
key: sample.key,
|
|
1231
|
+
item: {
|
|
1232
|
+
source: "sample",
|
|
1233
|
+
sample
|
|
959
1234
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1235
|
+
}));
|
|
1236
|
+
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1237
|
+
className: packagedCardClass,
|
|
1238
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1239
|
+
});
|
|
1240
|
+
/**
|
|
1241
|
+
* The product cards. `className` goes on the element that contains the cards, such as a grid. The loading
|
|
1242
|
+
* placeholder, the error with its retry, and the empty message render just before that element.
|
|
1243
|
+
*/
|
|
1244
|
+
function CatalogProducts({ className, children }) {
|
|
1245
|
+
const catalog = useCatalogInternal("CatalogProducts");
|
|
1246
|
+
const card = Children.toArray(children).some((child) => child !== "") ? children : packagedCard;
|
|
1247
|
+
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1248
|
+
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1249
|
+
key: product.id ?? "",
|
|
1250
|
+
item: {
|
|
1251
|
+
source: "live",
|
|
1252
|
+
product
|
|
1253
|
+
}
|
|
1254
|
+
})) : sampleItems, [nodes]);
|
|
1255
|
+
const products = catalog.source === "live" ? catalog.products : null;
|
|
1256
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1257
|
+
products?.isPending && /* @__PURE__ */ jsx("div", {
|
|
982
1258
|
"aria-hidden": "true",
|
|
983
1259
|
className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3",
|
|
984
1260
|
children: [
|
|
@@ -997,7 +1273,7 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
997
1273
|
})]
|
|
998
1274
|
}, item))
|
|
999
1275
|
}),
|
|
1000
|
-
products
|
|
1276
|
+
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1001
1277
|
role: "alert",
|
|
1002
1278
|
className: "rounded-lg bg-red-50 p-5 text-red-800",
|
|
1003
1279
|
children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
|
|
@@ -1007,17 +1283,30 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
1007
1283
|
children: "Retry products"
|
|
1008
1284
|
})]
|
|
1009
1285
|
}),
|
|
1010
|
-
products
|
|
1286
|
+
products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
|
|
1011
1287
|
className: "py-16 text-center",
|
|
1012
1288
|
children: "No products available."
|
|
1013
1289
|
}),
|
|
1014
1290
|
/* @__PURE__ */ jsx("div", {
|
|
1015
|
-
className:
|
|
1016
|
-
children:
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1291
|
+
className: partClass(className),
|
|
1292
|
+
children: items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1293
|
+
value: item,
|
|
1294
|
+
children: card
|
|
1295
|
+
}, key))
|
|
1296
|
+
})
|
|
1297
|
+
] });
|
|
1298
|
+
}
|
|
1299
|
+
/** First-page and next-page buttons. Renders nothing for samples, which have one page. */
|
|
1300
|
+
function CatalogPagination({ className }) {
|
|
1301
|
+
const catalog = useCatalogInternal("CatalogPagination");
|
|
1302
|
+
if (catalog.source === "sample") return null;
|
|
1303
|
+
const { after, goToPage } = catalog;
|
|
1304
|
+
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
1305
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
1306
|
+
"aria-label": "Product pages",
|
|
1307
|
+
className: partClass(className),
|
|
1308
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1309
|
+
className: "flex justify-between",
|
|
1021
1310
|
children: [after ? /* @__PURE__ */ jsx("button", {
|
|
1022
1311
|
type: "button",
|
|
1023
1312
|
className: buttonClass,
|
|
@@ -1030,60 +1319,21 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
1030
1319
|
children: "Next page"
|
|
1031
1320
|
})]
|
|
1032
1321
|
})
|
|
1033
|
-
|
|
1322
|
+
});
|
|
1034
1323
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return /* @__PURE__ */ jsxs(
|
|
1038
|
-
|
|
1324
|
+
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
1325
|
+
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
|
|
1326
|
+
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
1327
|
+
sampleProducts: sampleProducts$1,
|
|
1039
1328
|
children: [
|
|
1040
|
-
showHeader && /* @__PURE__ */
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
children: title
|
|
1045
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1046
|
-
className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
|
|
1047
|
-
children: description
|
|
1048
|
-
})]
|
|
1049
|
-
}),
|
|
1050
|
-
/* @__PURE__ */ jsx("p", {
|
|
1051
|
-
className: "border-b border-neutral-200 py-6 text-sm",
|
|
1052
|
-
role: "status",
|
|
1053
|
-
children: "Sample products · Example prices in USD"
|
|
1329
|
+
showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
1330
|
+
title,
|
|
1331
|
+
description,
|
|
1332
|
+
className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8"
|
|
1054
1333
|
}),
|
|
1055
|
-
/* @__PURE__ */ jsx("
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
className: "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
|
|
1059
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1060
|
-
"aria-hidden": "true",
|
|
1061
|
-
className: "flex aspect-square items-center justify-center bg-neutral-100",
|
|
1062
|
-
children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
|
|
1063
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1064
|
-
className: "flex flex-1 flex-col p-6",
|
|
1065
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
1066
|
-
className: "break-words text-2xl font-semibold leading-tight",
|
|
1067
|
-
children: product.name
|
|
1068
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1069
|
-
className: "mt-auto pt-6",
|
|
1070
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1071
|
-
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1072
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1073
|
-
className: "text-lg font-semibold leading-7",
|
|
1074
|
-
children: money(product.price, "USD")
|
|
1075
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1076
|
-
type: "button",
|
|
1077
|
-
className: buttonClass,
|
|
1078
|
-
"aria-label": `Add ${product.name} to sample cart`,
|
|
1079
|
-
onClick: () => add(product.key),
|
|
1080
|
-
children: "Add to cart"
|
|
1081
|
-
})]
|
|
1082
|
-
})
|
|
1083
|
-
})]
|
|
1084
|
-
})]
|
|
1085
|
-
}, product.key))
|
|
1086
|
-
})
|
|
1334
|
+
/* @__PURE__ */ jsx(CatalogSummary, { className: "border-b border-neutral-200 py-6 sm:py-8" }),
|
|
1335
|
+
/* @__PURE__ */ jsx(CatalogProducts, { className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10" }),
|
|
1336
|
+
/* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
|
|
1087
1337
|
]
|
|
1088
1338
|
});
|
|
1089
1339
|
}
|
|
@@ -1099,15 +1349,72 @@ function CommerceStorefront({ children,...props }) {
|
|
|
1099
1349
|
}
|
|
1100
1350
|
|
|
1101
1351
|
//#endregion
|
|
1102
|
-
//#region src/product-details.tsx
|
|
1103
|
-
|
|
1352
|
+
//#region src/product-details-context.tsx
|
|
1353
|
+
const ProductDetailsContext = createContext(null);
|
|
1354
|
+
function useProductDetailsInternal(part) {
|
|
1355
|
+
const context = useContext(ProductDetailsContext);
|
|
1356
|
+
if (!context) throw new Error(`${part} must be rendered inside <ProductDetailsRoot>.`);
|
|
1357
|
+
return context;
|
|
1358
|
+
}
|
|
1359
|
+
/** Read the current product, verified SKU, and purchase status from inside `ProductDetailsRoot`. */
|
|
1360
|
+
function useProductDetails() {
|
|
1361
|
+
const { product, name, selectedSku, quantity, availableQuantity, status, images } = useProductDetailsInternal("useProductDetails");
|
|
1362
|
+
return {
|
|
1363
|
+
product,
|
|
1364
|
+
name,
|
|
1365
|
+
selectedSku,
|
|
1366
|
+
quantity,
|
|
1367
|
+
availableQuantity,
|
|
1368
|
+
status,
|
|
1369
|
+
images
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Build a custom control for one product attribute. Returns `null` when the product has no such attribute.
|
|
1374
|
+
* A selection is never purchasable until the server verifies it.
|
|
1375
|
+
*/
|
|
1376
|
+
function useProductDetailsOption(attribute) {
|
|
1377
|
+
const { attributes, selections, selectOption, register } = useProductDetailsInternal("useProductDetailsOption");
|
|
1378
|
+
useEffect(() => register(`attribute:${attribute}`), [attribute, register]);
|
|
1379
|
+
const index = attributes.findIndex((candidate) => candidate.name === attribute);
|
|
1380
|
+
const match = attributes[index];
|
|
1381
|
+
if (!match) return null;
|
|
1382
|
+
const selected = selections[index] || null;
|
|
1383
|
+
return {
|
|
1384
|
+
name: match.name,
|
|
1385
|
+
label: match.label,
|
|
1386
|
+
selected,
|
|
1387
|
+
values: match.values.map((value) => ({
|
|
1388
|
+
name: value.name,
|
|
1389
|
+
label: value.label,
|
|
1390
|
+
selected: value.name === selected
|
|
1391
|
+
})),
|
|
1392
|
+
select: (value) => {
|
|
1393
|
+
if (value === null) selectOption(attribute, "");
|
|
1394
|
+
else if (match.values.some((candidate) => candidate.name === value)) selectOption(attribute, value);
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
/** Register the explicit SKU control so the layout check knows SKU-list products can be completed. */
|
|
1399
|
+
function useSkuOptionRegistration() {
|
|
1400
|
+
const { register } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1401
|
+
useEffect(() => register("sku"), [register]);
|
|
1402
|
+
}
|
|
1403
|
+
/** Loads the product for the route's `:productId` and provides state to product parts. */
|
|
1404
|
+
function ProductDetailsRoot({ children, className }) {
|
|
1104
1405
|
const { productId = "" } = useParams();
|
|
1105
1406
|
const { connection } = useCommerce();
|
|
1106
|
-
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(
|
|
1407
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(ProductDetailsLoader, {
|
|
1408
|
+
productId,
|
|
1409
|
+
className,
|
|
1410
|
+
children
|
|
1411
|
+
}, productId) : /* @__PURE__ */ jsx("div", {
|
|
1412
|
+
className: partClass(className),
|
|
1413
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1414
|
+
}) });
|
|
1107
1415
|
}
|
|
1108
|
-
function
|
|
1416
|
+
function ProductDetailsLoader({ productId, className, children }) {
|
|
1109
1417
|
const { config } = useCommerce();
|
|
1110
|
-
const fieldId = useId();
|
|
1111
1418
|
const [params, setParams] = useSearchParams();
|
|
1112
1419
|
const [quantity, setQuantity] = useState(1);
|
|
1113
1420
|
const [imageIndex, setImageIndex] = useState(0);
|
|
@@ -1130,10 +1437,10 @@ function ProductDetailsContent({ productId }) {
|
|
|
1130
1437
|
const skuOptions = getLabeledSkuOptions(group);
|
|
1131
1438
|
const explicitSku = skuOptions.find((sku) => sku.id === params.get("sku"));
|
|
1132
1439
|
const selections = attributes.map((attribute) => params.get(`option.${attribute.name}`) ?? "");
|
|
1133
|
-
const complete = skuOptions.length > 0 ? !!explicitSku : attributes.every((attribute, index) => attribute.values.some((value) => value.name === selections[index]));
|
|
1440
|
+
const complete = skuOptions.length > 0 ? !!explicitSku : attributes.every((attribute, index) => attribute.values.some((value$1) => value$1.name === selections[index]));
|
|
1134
1441
|
const selectionParams = new URLSearchParams();
|
|
1135
|
-
selections.filter(Boolean).forEach((value) => {
|
|
1136
|
-
selectionParams.append("attributeValues", value);
|
|
1442
|
+
selections.filter(Boolean).forEach((value$1) => {
|
|
1443
|
+
selectionParams.append("attributeValues", value$1);
|
|
1137
1444
|
});
|
|
1138
1445
|
const matched = useQuery({
|
|
1139
1446
|
retry: false,
|
|
@@ -1152,26 +1459,35 @@ function ProductDetailsContent({ productId }) {
|
|
|
1152
1459
|
});
|
|
1153
1460
|
const selectionVerified = complete && product.isSuccess && !product.isFetching && (attributes.length === 0 || matched.isSuccess && !matched.isFetching);
|
|
1154
1461
|
const selectedSku = selectionVerified ? explicitSku ?? getSingleMatchedSku(attributes.length ? matched.data?.skuGroup : group) : null;
|
|
1155
|
-
const
|
|
1156
|
-
const skuPrice = selectedSku?.prices?.edges?.find((edge) => edge?.node?.value)?.node;
|
|
1462
|
+
const status = !complete ? "incomplete" : matched.isError && !matched.isFetching ? "variant-error" : !selectionVerified ? "checking" : selectedSku ? "ready" : attributes.length ? "unavailable" : "not-purchasable";
|
|
1157
1463
|
const selectedImages = getImageUrls(selectedSku);
|
|
1158
1464
|
const images = [...new Set(selectedImages.length ? selectedImages : getImageUrls(group))];
|
|
1159
|
-
const
|
|
1160
|
-
const name = group?.label ?? group?.name ?? "Product";
|
|
1161
|
-
const selectOption = (attribute, value) => {
|
|
1465
|
+
const changeSelection = (key, value$1) => {
|
|
1162
1466
|
const next = new URLSearchParams(params);
|
|
1163
|
-
if (value) next.set(
|
|
1164
|
-
else next.delete(
|
|
1467
|
+
if (value$1) next.set(key, value$1);
|
|
1468
|
+
else next.delete(key);
|
|
1165
1469
|
setParams(next);
|
|
1166
1470
|
setQuantity(1);
|
|
1167
1471
|
setImageIndex(0);
|
|
1168
1472
|
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1473
|
+
const { register, has } = usePartRegistry();
|
|
1474
|
+
const requiredControls = [...attributes.map((attribute) => `attribute:${attribute.name}`), ...skuOptions.length ? ["sku"] : []].join("\n");
|
|
1475
|
+
useEffect(() => {
|
|
1476
|
+
if (process.env.NODE_ENV === "production") return;
|
|
1477
|
+
const missing = requiredControls.split("\n").filter((control) => control && !has(control));
|
|
1478
|
+
if (missing.length === 0) return;
|
|
1479
|
+
console.warn(`[gd-commerce-storefront] No control for ${missing.map((control) => control === "sku" ? "the SKU option" : `"${control.slice(10)}"`).join(", ")} is rendered right now, so this product cannot be added to the cart until one is. Render <ProductDetailsOptions />, <ProductDetailsOption attribute='…' /> for each attribute, or <ProductDetailsPurchase />, or ignore this if the control mounts later.`);
|
|
1480
|
+
}, [requiredControls, has]);
|
|
1481
|
+
if (product.isPending) return /* @__PURE__ */ jsx("div", {
|
|
1482
|
+
className: partClass(className),
|
|
1483
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1484
|
+
role: "status",
|
|
1485
|
+
children: "Loading product…"
|
|
1486
|
+
})
|
|
1172
1487
|
});
|
|
1173
1488
|
if (product.isError && !productNotFound) return /* @__PURE__ */ jsxs("div", {
|
|
1174
1489
|
role: "alert",
|
|
1490
|
+
className: partClass(className),
|
|
1175
1491
|
children: [
|
|
1176
1492
|
/* @__PURE__ */ jsx("h1", {
|
|
1177
1493
|
className: "text-2xl font-semibold",
|
|
@@ -1189,175 +1505,358 @@ function ProductDetailsContent({ productId }) {
|
|
|
1189
1505
|
})
|
|
1190
1506
|
]
|
|
1191
1507
|
});
|
|
1192
|
-
if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
|
|
1193
|
-
className:
|
|
1194
|
-
children:
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1508
|
+
if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
|
|
1509
|
+
className: partClass(className),
|
|
1510
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1511
|
+
className: "text-2xl font-semibold",
|
|
1512
|
+
children: "Product not found"
|
|
1513
|
+
}), /* @__PURE__ */ jsx(Link, {
|
|
1514
|
+
className: `${buttonClass} mt-4`,
|
|
1515
|
+
to: config.catalogPath,
|
|
1516
|
+
children: "Back to shop"
|
|
1517
|
+
})]
|
|
1518
|
+
});
|
|
1519
|
+
const value = {
|
|
1520
|
+
product: group,
|
|
1521
|
+
name: getProductName(group),
|
|
1522
|
+
selectedSku,
|
|
1523
|
+
quantity,
|
|
1524
|
+
availableQuantity: getAvailableInventoryQuantity(selectedSku),
|
|
1525
|
+
status,
|
|
1526
|
+
images,
|
|
1527
|
+
attributes,
|
|
1528
|
+
selections,
|
|
1529
|
+
skuOptions,
|
|
1530
|
+
explicitSkuId: explicitSku?.id ?? "",
|
|
1531
|
+
price: selectedSku?.prices?.edges?.find((edge) => edge?.node?.value)?.node,
|
|
1532
|
+
imageIndex: Math.min(imageIndex, Math.max(images.length - 1, 0)),
|
|
1533
|
+
variantError: matched.error,
|
|
1534
|
+
setImageIndex: (index) => {
|
|
1535
|
+
if (Number.isInteger(index) && index >= 0 && index < images.length) setImageIndex(index);
|
|
1536
|
+
},
|
|
1537
|
+
setQuantity,
|
|
1538
|
+
selectOption: (attribute, next) => changeSelection(`option.${attribute}`, next),
|
|
1539
|
+
selectSku: (skuId) => changeSelection("sku", skuOptions.some((sku) => sku.id === skuId) ? skuId : ""),
|
|
1540
|
+
retryVariant: () => {
|
|
1541
|
+
if (matched.isError) matched.refetch();
|
|
1542
|
+
},
|
|
1543
|
+
register
|
|
1544
|
+
};
|
|
1545
|
+
return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
|
|
1546
|
+
value,
|
|
1547
|
+
children: /* @__PURE__ */ jsx("article", {
|
|
1548
|
+
className: partClass(className),
|
|
1549
|
+
children
|
|
1550
|
+
})
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
//#endregion
|
|
1555
|
+
//#region src/product-purchase.tsx
|
|
1556
|
+
const disabledReasons = {
|
|
1557
|
+
incomplete: "Select all options to add this item to your cart.",
|
|
1558
|
+
checking: "Checking this variant…",
|
|
1559
|
+
"variant-error": "Availability for this selection could not be checked.",
|
|
1560
|
+
unavailable: "This combination is unavailable. Choose different options.",
|
|
1561
|
+
"not-purchasable": "This product is not currently available to purchase."
|
|
1562
|
+
};
|
|
1563
|
+
/** The packaged purchase area: options, status, availability, quantity, and add to cart. */
|
|
1564
|
+
function ProductDetailsPurchase({ className }) {
|
|
1565
|
+
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
1566
|
+
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
1567
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1568
|
+
className: partClass("space-y-6", className),
|
|
1569
|
+
children: [
|
|
1570
|
+
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
1571
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
1572
|
+
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className: belowOptions }),
|
|
1573
|
+
/* @__PURE__ */ jsx(ProductDetailsQuantity, {}),
|
|
1574
|
+
/* @__PURE__ */ jsx(ProductDetailsAddToCart, {})
|
|
1575
|
+
]
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
/** A select for every product attribute, or the SKU list for products without attributes. */
|
|
1579
|
+
function ProductDetailsOptions({ className }) {
|
|
1580
|
+
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1581
|
+
if (attributes.length === 0 && skuOptions.length === 0) return null;
|
|
1582
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1583
|
+
className: partClass("space-y-6", className),
|
|
1584
|
+
children: /* @__PURE__ */ jsx(OptionControls, {})
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
function OptionControls() {
|
|
1588
|
+
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1589
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
1590
|
+
}
|
|
1591
|
+
function Select({ children,...props }) {
|
|
1592
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1593
|
+
className: "relative",
|
|
1594
|
+
children: [/* @__PURE__ */ jsx("select", {
|
|
1595
|
+
className: `${inputClass} w-full appearance-none pr-10`,
|
|
1596
|
+
...props,
|
|
1597
|
+
children
|
|
1598
|
+
}), /* @__PURE__ */ jsx("svg", {
|
|
1599
|
+
"aria-hidden": "true",
|
|
1600
|
+
viewBox: "0 0 16 16",
|
|
1601
|
+
className: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-600",
|
|
1602
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1603
|
+
d: "M4 6l4 4 4-4",
|
|
1604
|
+
fill: "none",
|
|
1605
|
+
stroke: "currentColor",
|
|
1606
|
+
strokeWidth: "1.5",
|
|
1607
|
+
strokeLinecap: "round",
|
|
1608
|
+
strokeLinejoin: "round"
|
|
1609
|
+
})
|
|
1610
|
+
})]
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
function SkuOption() {
|
|
1614
|
+
const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1615
|
+
const id = useId();
|
|
1616
|
+
useSkuOptionRegistration();
|
|
1617
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1618
|
+
className: partClass(),
|
|
1619
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
1620
|
+
htmlFor: id,
|
|
1621
|
+
className: "mb-2 block font-medium",
|
|
1622
|
+
children: "Option"
|
|
1623
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1624
|
+
id,
|
|
1625
|
+
value: explicitSkuId,
|
|
1626
|
+
onChange: (event) => selectSku(event.target.value),
|
|
1627
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
1628
|
+
value: "",
|
|
1629
|
+
children: "Select an option"
|
|
1630
|
+
}), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
|
|
1631
|
+
value: sku.id ?? "",
|
|
1632
|
+
children: sku.label?.trim() || sku.name?.trim()
|
|
1633
|
+
}, sku.id))]
|
|
1634
|
+
})]
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
/** A select for one product attribute. Use `useProductDetailsOption` for swatches or other custom controls. */
|
|
1638
|
+
function ProductDetailsOption({ attribute, className }) {
|
|
1639
|
+
const option = useProductDetailsOption(attribute);
|
|
1640
|
+
if (!option) return null;
|
|
1641
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1642
|
+
className: partClass(className),
|
|
1643
|
+
children: /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
|
|
1644
|
+
className: "mb-2 font-medium",
|
|
1645
|
+
children: option.label
|
|
1646
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1647
|
+
"aria-label": option.label,
|
|
1648
|
+
value: option.selected ?? "",
|
|
1649
|
+
onChange: (event) => option.select(event.target.value || null),
|
|
1650
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
1651
|
+
value: "",
|
|
1652
|
+
children: ["Select ", option.label.toLowerCase()]
|
|
1653
|
+
}), option.values.map((value) => /* @__PURE__ */ jsx("option", {
|
|
1654
|
+
value: value.name,
|
|
1655
|
+
children: value.label
|
|
1656
|
+
}, value.name))]
|
|
1657
|
+
})] })
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
/** The single message explaining why the selection is not yet purchasable. Renders nothing when ready. */
|
|
1661
|
+
function ProductDetailsPurchaseStatus({ className }) {
|
|
1662
|
+
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
1663
|
+
let content;
|
|
1664
|
+
switch (status) {
|
|
1665
|
+
case "ready": return null;
|
|
1666
|
+
case "variant-error":
|
|
1667
|
+
content = /* @__PURE__ */ jsxs("div", {
|
|
1668
|
+
role: "alert",
|
|
1669
|
+
className: "text-red-700",
|
|
1670
|
+
children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
|
|
1217
1671
|
type: "button",
|
|
1218
|
-
className:
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1672
|
+
className: "min-h-11 bg-white text-red-700 underline",
|
|
1673
|
+
onClick: retryVariant,
|
|
1674
|
+
children: "Retry availability"
|
|
1675
|
+
})]
|
|
1676
|
+
});
|
|
1677
|
+
break;
|
|
1678
|
+
case "unavailable":
|
|
1679
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
1680
|
+
role: "status",
|
|
1681
|
+
className: "text-sm text-red-700",
|
|
1682
|
+
children: disabledReasons[status]
|
|
1683
|
+
});
|
|
1684
|
+
break;
|
|
1685
|
+
case "not-purchasable":
|
|
1686
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
1687
|
+
className: "text-sm",
|
|
1688
|
+
children: disabledReasons[status]
|
|
1689
|
+
});
|
|
1690
|
+
break;
|
|
1691
|
+
case "incomplete":
|
|
1692
|
+
case "checking":
|
|
1693
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
1694
|
+
role: "status",
|
|
1695
|
+
className: "text-sm",
|
|
1696
|
+
children: disabledReasons[status]
|
|
1697
|
+
});
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1701
|
+
className: partClass(className),
|
|
1702
|
+
children: content
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
/** Stock for the verified SKU. Renders nothing until a SKU is verified. */
|
|
1706
|
+
function ProductDetailsAvailability({ className }) {
|
|
1707
|
+
const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
|
|
1708
|
+
if (!selectedSku) return null;
|
|
1709
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1710
|
+
className: partClass(className),
|
|
1711
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1712
|
+
className: "text-sm",
|
|
1713
|
+
"aria-live": "polite",
|
|
1714
|
+
children: availableQuantity === null ? "Available to order" : availableQuantity === 0 ? "This variant is out of stock" : `${availableQuantity} available`
|
|
1715
|
+
})
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
/** Quantity for the verified SKU. Renders nothing until a SKU is verified. */
|
|
1719
|
+
function ProductDetailsQuantity({ className }) {
|
|
1720
|
+
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
1721
|
+
const id = useId();
|
|
1722
|
+
if (!selectedSku) return null;
|
|
1723
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1724
|
+
className: partClass(className),
|
|
1725
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
1726
|
+
htmlFor: id,
|
|
1727
|
+
className: "mb-2 block font-medium",
|
|
1728
|
+
children: "Quantity"
|
|
1729
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1730
|
+
id,
|
|
1731
|
+
type: "number",
|
|
1732
|
+
min: 1,
|
|
1733
|
+
max: availableQuantity ?? void 0,
|
|
1734
|
+
step: 1,
|
|
1735
|
+
className: `${inputClass} w-28`,
|
|
1736
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
1737
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
1738
|
+
})]
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
/** Adds the verified SKU and quantity. While disabled, its accessible description says why. */
|
|
1742
|
+
function ProductDetailsAddToCart({ className }) {
|
|
1743
|
+
const { selectedSku, name, quantity, status } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
1744
|
+
const reasonId = useId();
|
|
1745
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1746
|
+
className: partClass(className),
|
|
1747
|
+
children: selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
|
|
1748
|
+
sku: selectedSku,
|
|
1749
|
+
name,
|
|
1750
|
+
quantity
|
|
1751
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
1752
|
+
type: "button",
|
|
1753
|
+
className: `${buttonClass} w-full`,
|
|
1754
|
+
disabled: true,
|
|
1755
|
+
"aria-describedby": reasonId,
|
|
1756
|
+
children: "Add to cart"
|
|
1757
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1758
|
+
id: reasonId,
|
|
1759
|
+
hidden: true,
|
|
1760
|
+
children: status === "ready" ? "" : disabledReasons[status]
|
|
1761
|
+
})] })
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/product-details.tsx
|
|
1767
|
+
/** The packaged product page for `${productPath}/:productId`. Compose the parts for a custom layout. */
|
|
1768
|
+
function ProductDetails() {
|
|
1769
|
+
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-8 sm:mb-10" }), /* @__PURE__ */ jsxs("div", {
|
|
1770
|
+
className: "grid items-start gap-8 lg:grid-cols-12 lg:gap-12",
|
|
1771
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsGallery, { className: "min-w-0 lg:col-span-7" }), /* @__PURE__ */ jsxs("div", {
|
|
1229
1772
|
className: "min-w-0 lg:col-span-5 lg:py-4",
|
|
1230
1773
|
children: [
|
|
1231
|
-
/* @__PURE__ */ jsx(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
})
|
|
1235
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1236
|
-
"aria-live": "polite",
|
|
1237
|
-
className: "mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
|
|
1238
|
-
"data-testid": "product-price",
|
|
1239
|
-
children: [skuPrice?.value?.value != null ? money(skuPrice.value.value, skuPrice.value.currencyCode ?? config.currencyCode) : group.priceRange?.min != null ? `From ${money(group.priceRange.min, config.currencyCode)}` : "Price unavailable", skuPrice?.compareAtValue?.value != null && skuPrice.value?.value != null && skuPrice.compareAtValue.value > skuPrice.value.value && /* @__PURE__ */ jsx("del", {
|
|
1240
|
-
className: "text-lg font-normal",
|
|
1241
|
-
children: money(skuPrice.compareAtValue.value, skuPrice.compareAtValue.currencyCode ?? config.currencyCode)
|
|
1242
|
-
})]
|
|
1243
|
-
}),
|
|
1244
|
-
/* @__PURE__ */ jsx("p", {
|
|
1245
|
-
className: "my-8 whitespace-pre-line text-base leading-7",
|
|
1246
|
-
children: selectedSku?.description ?? group.description
|
|
1247
|
-
}),
|
|
1248
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1249
|
-
className: "space-y-6 border-t border-neutral-200 pt-8",
|
|
1250
|
-
children: [
|
|
1251
|
-
attributes.map((attribute, index) => /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
|
|
1252
|
-
className: "mb-2 font-medium",
|
|
1253
|
-
children: attribute.label
|
|
1254
|
-
}), /* @__PURE__ */ jsxs("select", {
|
|
1255
|
-
className: `${inputClass} w-full`,
|
|
1256
|
-
"aria-label": attribute.label,
|
|
1257
|
-
value: selections[index],
|
|
1258
|
-
onChange: (event) => selectOption(attribute.name, event.target.value),
|
|
1259
|
-
children: [/* @__PURE__ */ jsxs("option", {
|
|
1260
|
-
value: "",
|
|
1261
|
-
children: ["Select ", attribute.label.toLowerCase()]
|
|
1262
|
-
}), attribute.values.map((value) => /* @__PURE__ */ jsx("option", {
|
|
1263
|
-
value: value.name,
|
|
1264
|
-
children: value.label
|
|
1265
|
-
}, value.id || value.name))]
|
|
1266
|
-
})] }, attribute.id || attribute.name)),
|
|
1267
|
-
skuOptions.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
1268
|
-
htmlFor: `${fieldId}-sku-option`,
|
|
1269
|
-
className: "mb-2 block font-medium",
|
|
1270
|
-
children: "Option"
|
|
1271
|
-
}), /* @__PURE__ */ jsxs("select", {
|
|
1272
|
-
id: `${fieldId}-sku-option`,
|
|
1273
|
-
className: `${inputClass} w-full`,
|
|
1274
|
-
value: explicitSku?.id ?? "",
|
|
1275
|
-
onChange: (event) => {
|
|
1276
|
-
const next = new URLSearchParams(params);
|
|
1277
|
-
if (event.target.value) next.set("sku", event.target.value);
|
|
1278
|
-
else next.delete("sku");
|
|
1279
|
-
setParams(next);
|
|
1280
|
-
setQuantity(1);
|
|
1281
|
-
setImageIndex(0);
|
|
1282
|
-
},
|
|
1283
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
1284
|
-
value: "",
|
|
1285
|
-
children: "Select an option"
|
|
1286
|
-
}), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
|
|
1287
|
-
value: sku.id ?? "",
|
|
1288
|
-
children: sku.label?.trim() || sku.name?.trim()
|
|
1289
|
-
}, sku.id))]
|
|
1290
|
-
})] }),
|
|
1291
|
-
matched.isError ? /* @__PURE__ */ jsxs("div", {
|
|
1292
|
-
role: "alert",
|
|
1293
|
-
className: "text-red-700",
|
|
1294
|
-
children: [/* @__PURE__ */ jsx("p", { children: message(matched.error) }), /* @__PURE__ */ jsx("button", {
|
|
1295
|
-
type: "button",
|
|
1296
|
-
className: "min-h-11 bg-white text-red-700 underline",
|
|
1297
|
-
onClick: () => {
|
|
1298
|
-
if (matched.isError) matched.refetch();
|
|
1299
|
-
},
|
|
1300
|
-
children: "Retry availability"
|
|
1301
|
-
})]
|
|
1302
|
-
}) : null,
|
|
1303
|
-
!complete && /* @__PURE__ */ jsx("p", {
|
|
1304
|
-
id: `${fieldId}-variant-guidance`,
|
|
1305
|
-
role: "status",
|
|
1306
|
-
className: "text-sm",
|
|
1307
|
-
children: "Select all options to add this item to your cart."
|
|
1308
|
-
}),
|
|
1309
|
-
selectionVerified && attributes.length > 0 && !skuId && /* @__PURE__ */ jsx("p", {
|
|
1310
|
-
role: "status",
|
|
1311
|
-
className: "text-sm text-red-700",
|
|
1312
|
-
children: "This combination is unavailable. Choose different options."
|
|
1313
|
-
}),
|
|
1314
|
-
selectionVerified && attributes.length === 0 && !skuId && /* @__PURE__ */ jsx("p", {
|
|
1315
|
-
className: "text-sm",
|
|
1316
|
-
children: "This product is not currently available to purchase."
|
|
1317
|
-
}),
|
|
1318
|
-
complete && (product.isFetching || matched.isFetching) && /* @__PURE__ */ jsx("p", {
|
|
1319
|
-
role: "status",
|
|
1320
|
-
children: "Checking this variant…"
|
|
1321
|
-
}),
|
|
1322
|
-
selectedSku && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1323
|
-
/* @__PURE__ */ jsx("p", {
|
|
1324
|
-
className: "text-sm",
|
|
1325
|
-
"aria-live": "polite",
|
|
1326
|
-
children: available === null ? "Available to order" : available === 0 ? "This variant is out of stock" : `${available} available`
|
|
1327
|
-
}),
|
|
1328
|
-
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
1329
|
-
htmlFor: `${fieldId}-quantity`,
|
|
1330
|
-
className: "mb-2 block font-medium",
|
|
1331
|
-
children: "Quantity"
|
|
1332
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
1333
|
-
id: `${fieldId}-quantity`,
|
|
1334
|
-
type: "number",
|
|
1335
|
-
min: 1,
|
|
1336
|
-
max: available ?? void 0,
|
|
1337
|
-
step: 1,
|
|
1338
|
-
className: `${inputClass} w-28`,
|
|
1339
|
-
value: Number.isNaN(quantity) ? "" : quantity,
|
|
1340
|
-
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
1341
|
-
})] }),
|
|
1342
|
-
/* @__PURE__ */ jsx(AddToCartButton, {
|
|
1343
|
-
sku: selectedSku,
|
|
1344
|
-
name,
|
|
1345
|
-
quantity
|
|
1346
|
-
})
|
|
1347
|
-
] }),
|
|
1348
|
-
!selectedSku && /* @__PURE__ */ jsx("button", {
|
|
1349
|
-
type: "button",
|
|
1350
|
-
className: `${buttonClass} w-full`,
|
|
1351
|
-
disabled: true,
|
|
1352
|
-
"aria-describedby": !complete ? `${fieldId}-variant-guidance` : void 0,
|
|
1353
|
-
children: "Add to cart"
|
|
1354
|
-
})
|
|
1355
|
-
]
|
|
1356
|
-
})
|
|
1774
|
+
/* @__PURE__ */ jsx(ProductDetailsTitle, {}),
|
|
1775
|
+
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-6" }),
|
|
1776
|
+
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "my-8" }),
|
|
1777
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "border-t border-neutral-200 pt-8" })
|
|
1357
1778
|
]
|
|
1358
1779
|
})]
|
|
1359
1780
|
})] });
|
|
1360
1781
|
}
|
|
1782
|
+
function ProductDetailsBackLink({ className }) {
|
|
1783
|
+
useProductDetailsInternal("ProductDetailsBackLink");
|
|
1784
|
+
const { config } = useCommerce();
|
|
1785
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1786
|
+
className: partClass(className),
|
|
1787
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
1788
|
+
className: "inline-flex min-h-11 items-center text-sm text-inherit underline underline-offset-4 focus-visible:outline focus-visible:outline-2",
|
|
1789
|
+
to: config.catalogPath,
|
|
1790
|
+
children: "← Back to shop"
|
|
1791
|
+
})
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
/** Main image with thumbnails; the selected SKU's images replace the product's when present. */
|
|
1795
|
+
function ProductDetailsGallery({ className }) {
|
|
1796
|
+
const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
|
|
1797
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1798
|
+
className: partClass(className),
|
|
1799
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1800
|
+
className: "aspect-square overflow-hidden rounded-lg",
|
|
1801
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1802
|
+
url: images[imageIndex],
|
|
1803
|
+
name
|
|
1804
|
+
}, images[imageIndex])
|
|
1805
|
+
}), images.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
1806
|
+
className: "mt-4 flex gap-3 overflow-x-auto",
|
|
1807
|
+
children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
|
|
1808
|
+
type: "button",
|
|
1809
|
+
className: `h-20 w-20 shrink-0 overflow-hidden rounded-lg border-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${imageIndex === index ? "border-neutral-900" : "border-transparent"}`,
|
|
1810
|
+
"aria-label": `View image ${index + 1}`,
|
|
1811
|
+
"aria-pressed": imageIndex === index,
|
|
1812
|
+
onClick: () => setImageIndex(index),
|
|
1813
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1814
|
+
url,
|
|
1815
|
+
name: ""
|
|
1816
|
+
})
|
|
1817
|
+
}, url))
|
|
1818
|
+
})]
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
/** The product name as the page's H1. */
|
|
1822
|
+
function ProductDetailsTitle({ className }) {
|
|
1823
|
+
const { name } = useProductDetailsInternal("ProductDetailsTitle");
|
|
1824
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1825
|
+
className: partClass(className),
|
|
1826
|
+
children: /* @__PURE__ */ jsx("h1", {
|
|
1827
|
+
className: "break-words text-4xl font-semibold leading-tight sm:text-5xl",
|
|
1828
|
+
children: name
|
|
1829
|
+
})
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
/** The verified SKU's price and compare-at price, or the product's starting price. */
|
|
1833
|
+
function ProductDetailsPrice({ className }) {
|
|
1834
|
+
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
1835
|
+
const { config } = useCommerce();
|
|
1836
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1837
|
+
className: partClass(className),
|
|
1838
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1839
|
+
"aria-live": "polite",
|
|
1840
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
|
|
1841
|
+
"data-testid": "product-price",
|
|
1842
|
+
children: [price?.value?.value != null ? money(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money(product.priceRange.min, config.currencyCode)}` : "Price unavailable", price?.compareAtValue?.value != null && price.value?.value != null && price.compareAtValue.value > price.value.value && /* @__PURE__ */ jsx("del", {
|
|
1843
|
+
className: "text-lg font-normal",
|
|
1844
|
+
children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
|
|
1845
|
+
})]
|
|
1846
|
+
})
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
/** The verified SKU's description, or the product's. */
|
|
1850
|
+
function ProductDetailsDescription({ className }) {
|
|
1851
|
+
const { selectedSku, product } = useProductDetailsInternal("ProductDetailsDescription");
|
|
1852
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1853
|
+
className: partClass(className),
|
|
1854
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1855
|
+
className: "whitespace-pre-line text-base leading-7",
|
|
1856
|
+
children: selectedSku?.description ?? product.description
|
|
1857
|
+
})
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1361
1860
|
|
|
1362
1861
|
//#endregion
|
|
1363
|
-
export { AddToCartButton, ApiError, CartButton, CartDrawer, Catalog, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductDetails, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCommerce };
|
|
1862
|
+
export { AddToCartButton, ApiError, CartButton, CartDrawer, Catalog, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCommerce, useProductCard, useProductDetails, useProductDetailsOption };
|