@asteby/storefront-react 0.4.0 → 0.5.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/dist/index.cjs CHANGED
@@ -502,6 +502,7 @@ function useProductOptions(product, config = {}) {
502
502
  exists: exact ? true : loose.length > 0 && Object.keys(selectedOptions).length < product.options.length,
503
503
  available: exact ? exact.available_for_sale : Object.keys(selectedOptions).length < product.options.length && loose.some((v) => v.available_for_sale),
504
504
  variant: exact,
505
+ availability: exact ? storefront.resolveAvailability(exact) : loose.length ? storefront.productAvailability({ variants: loose, available_for_sale: loose.some((v) => v.available_for_sale) }) : null,
505
506
  search: `?${search.toString()}`
506
507
  };
507
508
  })
@@ -520,21 +521,32 @@ function toMerchandise(product, variant) {
520
521
  function AddToCartButton({
521
522
  variantId,
522
523
  quantity = 1,
524
+ availability,
525
+ availabilitySource,
526
+ lowStockThreshold,
527
+ locale,
523
528
  merchandise,
524
529
  onAdded,
525
530
  onClick,
526
531
  disabled,
527
- children = "Agregar al carrito",
532
+ children,
528
533
  type = "button",
529
534
  ...rest
530
535
  }) {
531
536
  const cart = useCart();
537
+ const sf = useOptionalStorefront();
538
+ const resolved = availability ?? (availabilitySource ? storefront.resolveAvailability(availabilitySource, { lowStockThreshold: lowStockThreshold ?? null }) : null);
539
+ const view = resolved ? storefront.availabilityView(resolved, locale ?? sf?.locale ?? "es-MX") : null;
540
+ const blocked = view ? !view.ctaEnabled : false;
541
+ const qty = view?.maxPerOrder != null ? Math.min(quantity, view.maxPerOrder) : quantity;
542
+ const label = children ?? view?.ctaLabel ?? "Agregar al carrito";
532
543
  const [adding, setAdding] = react.useState(false);
533
544
  return /* @__PURE__ */ jsxRuntime.jsx(
534
545
  "button",
535
546
  {
536
547
  type,
537
- disabled: disabled || !variantId || adding,
548
+ disabled: disabled || !variantId || adding || blocked,
549
+ "data-availability": view?.status,
538
550
  "aria-busy": adding || void 0,
539
551
  "data-adding": adding || void 0,
540
552
  onClick: async (event) => {
@@ -542,17 +554,247 @@ function AddToCartButton({
542
554
  if (event.defaultPrevented || !variantId) return;
543
555
  setAdding(true);
544
556
  try {
545
- const result = await cart.addLines([{ variant_id: variantId, quantity, ...merchandise ? { merchandise } : {} }]);
557
+ const result = await cart.addLines([{ variant_id: variantId, quantity: qty, ...merchandise ? { merchandise } : {} }]);
546
558
  if (result) onAdded?.();
547
559
  } finally {
548
560
  setAdding(false);
549
561
  }
550
562
  },
551
563
  ...rest,
552
- children
564
+ children: label
553
565
  }
554
566
  );
555
567
  }
568
+ var SR_ONLY = {
569
+ position: "absolute",
570
+ width: 1,
571
+ height: 1,
572
+ margin: -1,
573
+ padding: 0,
574
+ overflow: "hidden",
575
+ clip: "rect(0 0 0 0)",
576
+ whiteSpace: "nowrap",
577
+ border: 0
578
+ };
579
+ function SrOnly({ children }) {
580
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: SR_ONLY, children });
581
+ }
582
+ function useLocale(locale) {
583
+ const sf = useOptionalStorefront();
584
+ return locale ?? sf?.locale ?? "es-MX";
585
+ }
586
+ function pickAvailability({ availability, source, lowStockThreshold }) {
587
+ const opts = { lowStockThreshold: lowStockThreshold ?? null };
588
+ if (availability) return availability;
589
+ return source ? storefront.resolveAvailability(source, opts) : null;
590
+ }
591
+ function useAvailability(input) {
592
+ const locale = useLocale(input.locale);
593
+ const a = pickAvailability(input);
594
+ return react.useMemo(
595
+ () => a ? storefront.availabilityView(a, locale, { messages: input.messages ?? null }) : null,
596
+ // `a` is rebuilt from props each render; key by its content
597
+ [JSON.stringify(a), locale, input.messages]
598
+ );
599
+ }
600
+ function AvailabilityBadge({
601
+ availability,
602
+ source,
603
+ lowStockThreshold,
604
+ locale,
605
+ messages,
606
+ placement = "detail",
607
+ live,
608
+ ...rest
609
+ }) {
610
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
611
+ const loc = useLocale(locale);
612
+ if (!view) return null;
613
+ if (placement === "card" && !view.showOnCard) return null;
614
+ const m = storefront.availabilityMessages(loc, messages);
615
+ return /* @__PURE__ */ jsxRuntime.jsxs(
616
+ "span",
617
+ {
618
+ "data-availability": view.status,
619
+ "data-tone": view.tone,
620
+ "data-placement": placement,
621
+ ...live ? { role: "status" } : {},
622
+ ...rest,
623
+ children: [
624
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": view.srText !== view.label ? true : void 0, children: view.label }),
625
+ view.srText !== view.label && /* @__PURE__ */ jsxRuntime.jsx(SrOnly, { children: view.srText }),
626
+ placement === "detail" && view.sublabel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
627
+ " ",
628
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability-note": "eta", children: view.sublabel })
629
+ ] }),
630
+ placement === "detail" && view.maxPerOrder !== null && view.status !== "out_of_stock" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
631
+ " ",
632
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability-note": "max", children: storefront.formatAvailabilityMessage(m.max_per_order, { n: view.maxPerOrder }) })
633
+ ] })
634
+ ]
635
+ }
636
+ );
637
+ }
638
+ function VariantOptionState({ children, availability, source, lowStockThreshold, locale, messages, ...rest }) {
639
+ const a = pickAvailability({ availability, source, lowStockThreshold });
640
+ const loc = useLocale(locale);
641
+ if (!a) return /* @__PURE__ */ jsxRuntime.jsx("span", { ...rest, children });
642
+ const m = storefront.availabilityMessages(loc, messages);
643
+ const status = storefront.availabilityView(a, loc, { messages: messages ?? null }).status;
644
+ if (status === "out_of_stock") {
645
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability": status, ...rest, children: [
646
+ /* @__PURE__ */ jsxRuntime.jsx("s", { children }),
647
+ /* @__PURE__ */ jsxRuntime.jsxs(SrOnly, { children: [
648
+ " (",
649
+ m["option.sold_out"],
650
+ ")"
651
+ ] })
652
+ ] });
653
+ }
654
+ if (status === "backorder" || status === "preorder") {
655
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability": status, ...rest, children: [
656
+ children,
657
+ " ",
658
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability-tag": true, children: [
659
+ "(",
660
+ m[status === "backorder" ? "option.backorder" : "option.preorder"],
661
+ ")"
662
+ ] })
663
+ ] });
664
+ }
665
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability": status, ...rest, children });
666
+ }
667
+ function RestockNotifyForm({
668
+ sku,
669
+ market,
670
+ allowPhone = false,
671
+ action,
672
+ client: clientProp,
673
+ enhance = true,
674
+ onSubscribed,
675
+ onError,
676
+ availability,
677
+ source,
678
+ lowStockThreshold,
679
+ locale,
680
+ messages,
681
+ ...rest
682
+ }) {
683
+ const sf = useOptionalStorefront();
684
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
685
+ const loc = useLocale(locale);
686
+ const id = react.useId();
687
+ const [state, setState] = react.useState("idle");
688
+ if (!view?.showNotify) return null;
689
+ const m = storefront.availabilityMessages(loc, messages);
690
+ const client = clientProp ?? sf?.client ?? null;
691
+ if (state === "done") {
692
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", "data-restock": "done", children: m["notify.done"] });
693
+ }
694
+ const submit = enhance && client ? (e) => {
695
+ e.preventDefault();
696
+ const form = new FormData(e.currentTarget);
697
+ const email = String(form.get("email") ?? "").trim();
698
+ const phone = String(form.get("phone") ?? "").trim();
699
+ if (!email && !phone) {
700
+ setState("error");
701
+ return;
702
+ }
703
+ setState("sending");
704
+ client.restock.subscribe({ sku, locale: loc, consent: true, ...email ? { email } : {}, ...phone ? { phone } : {}, ...market ? { market } : {} }).then(() => {
705
+ setState("done");
706
+ onSubscribed?.();
707
+ }).catch((err) => {
708
+ setState("error");
709
+ onError?.(err);
710
+ });
711
+ } : void 0;
712
+ return /* @__PURE__ */ jsxRuntime.jsx("form", { method: "post", ...action !== void 0 ? { action } : {}, ...rest, onSubmit: submit, "data-restock": state, children: /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
713
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: m["notify.title"] }),
714
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "sku", value: sku }),
715
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "locale", value: loc }),
716
+ market && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "market", value: market }),
717
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
718
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-email`, children: m["notify.email"] }),
719
+ " ",
720
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-email`, name: "email", type: "email", autoComplete: "email", required: !allowPhone })
721
+ ] }),
722
+ allowPhone && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
723
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-phone`, children: m["notify.phone"] }),
724
+ " ",
725
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-phone`, name: "phone", type: "tel", autoComplete: "tel" })
726
+ ] }),
727
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
728
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-consent`, name: "consent", type: "checkbox", value: "true", required: true }),
729
+ " ",
730
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-consent`, children: m["notify.consent"] })
731
+ ] }),
732
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: state === "sending", "aria-busy": state === "sending" || void 0, children: m["notify.submit"] }),
733
+ state === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: m["notify.error"] })
734
+ ] }) });
735
+ }
736
+ function CartLineProblem({ line, locale, messages, onAdjust, onRemove, ...rest }) {
737
+ const cart = useOptionalCart();
738
+ const loc = useLocale(locale);
739
+ if (!line.problem) return null;
740
+ const view = storefront.cartLineProblemView(line.problem, loc, { messages: messages ?? null, lineQuantity: line.quantity });
741
+ const adjust = onAdjust ?? (cart ? (id, q) => cart.updateLines([{ id, quantity: q }]) : void 0);
742
+ const remove = onRemove ?? (cart ? (id) => cart.removeLines([id]) : void 0);
743
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cart-line-problem": view.code, ...rest, children: [
744
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: view.message }),
745
+ view.adjustTo !== null && adjust && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void adjust(line.id, view.adjustTo), children: view.adjustLabel }),
746
+ remove && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void remove(line.id), children: view.removeLabel })
747
+ ] });
748
+ }
749
+ function StockConflictAlert({
750
+ error: errorProp,
751
+ cart: cartProp,
752
+ locale,
753
+ messages,
754
+ onAdjust,
755
+ onRemove,
756
+ onDismiss,
757
+ autoFocus = true,
758
+ ...rest
759
+ }) {
760
+ const cartCtx = useOptionalCart();
761
+ const loc = useLocale(locale);
762
+ const ref = react.useRef(null);
763
+ const error = errorProp !== void 0 ? errorProp : cartCtx?.error ?? null;
764
+ const cart = cartProp !== void 0 ? cartProp : cartCtx?.cart ?? null;
765
+ const active = storefront.isStorefrontError(error) && error.code === "insufficient_stock";
766
+ react.useEffect(() => {
767
+ if (active && autoFocus) ref.current?.focus({ preventScroll: true });
768
+ }, [active, error, autoFocus]);
769
+ if (!active) return null;
770
+ const m = storefront.availabilityMessages(loc, messages);
771
+ const lines = error.stockLines;
772
+ const adjust = onAdjust ?? (cartCtx ? (id, q) => cartCtx.updateLines([{ id, quantity: q }]) : void 0);
773
+ const remove = onRemove ?? (cartCtx ? (id) => cartCtx.removeLines([id]) : void 0);
774
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, role: "alert", tabIndex: -1, "data-stock-conflict": true, ...rest, children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: m["conflict.title"] }) }),
776
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["conflict.body"] }),
777
+ lines.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { children: lines.map((l) => {
778
+ const cartLine = cart?.lines.find((x) => l.line_id ? x.id === l.line_id : x.merchandise.sku === l.sku);
779
+ const lineId = l.line_id ?? cartLine?.id ?? null;
780
+ const view = storefront.cartLineProblemView(l, loc, { messages: messages ?? null, ...cartLine ? { lineQuantity: cartLine.quantity } : {} });
781
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { "data-cart-line-problem": view.code, children: [
782
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: cartLine ? `${cartLine.merchandise.product.title}${cartLine.merchandise.title ? ` (${cartLine.merchandise.title})` : ""}` : l.sku }),
783
+ ": ",
784
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: view.message }),
785
+ lineId && view.adjustTo !== null && adjust && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
786
+ " ",
787
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void adjust(lineId, view.adjustTo), children: view.adjustLabel })
788
+ ] }),
789
+ lineId && remove && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
790
+ " ",
791
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void remove(lineId), children: view.removeLabel })
792
+ ] })
793
+ ] }, `${l.sku}:${l.line_id ?? ""}`);
794
+ }) }),
795
+ onDismiss && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onDismiss, children: m["conflict.dismiss"] })
796
+ ] });
797
+ }
556
798
  var clean = (s, max) => {
557
799
  if (!s) return void 0;
558
800
  const text = s.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
@@ -586,7 +828,8 @@ function seoMeta(input) {
586
828
  function seoOf(seo, fallbackTitle, fallbackDescription) {
587
829
  return { title: seo.title ?? fallbackTitle, description: seo.description ?? fallbackDescription ?? null };
588
830
  }
589
- function productMeta(product, shop, url) {
831
+ var OG_AVAILABILITY = { in_stock: "in stock", low_stock: "in stock", out_of_stock: "out of stock", backorder: "backorder", preorder: "preorder" };
832
+ function productMeta(product, shop, url, options = {}) {
590
833
  const { title, description } = seoOf(product.seo, product.title, product.description);
591
834
  const meta = seoMeta({
592
835
  title,
@@ -601,7 +844,7 @@ function productMeta(product, shop, url) {
601
844
  meta.push(
602
845
  { property: "product:price:amount", content: product.price_range.min.amount },
603
846
  { property: "product:price:currency", content: product.price_range.min.currency_code },
604
- { property: "product:availability", content: product.available_for_sale ? "in stock" : "out of stock" }
847
+ { property: "product:availability", content: OG_AVAILABILITY[storefront.productAvailability(product, options).status] ?? "out of stock" }
605
848
  );
606
849
  if (product.vendor) meta.push({ property: "product:brand", content: product.vendor });
607
850
  return meta;
@@ -638,22 +881,24 @@ function withVariant(url, variantId) {
638
881
  return `${url}${url.includes("?") ? "&" : "?"}variant=${encodeURIComponent(variantId)}`;
639
882
  }
640
883
  }
641
- function productJsonLd(product, shop, url) {
884
+ function productJsonLd(product, shop, url, options = {}) {
642
885
  const images = product.images.length ? product.images.map((i) => i.url) : product.featured_image ? [product.featured_image.url] : [];
643
- const offers = product.variants.map(
644
- (v) => compact({
886
+ const offers = product.variants.map((v) => {
887
+ const availability = storefront.resolveAvailability(v, options);
888
+ return compact({
645
889
  "@type": "Offer",
646
890
  sku: v.sku,
647
891
  gtin: v.barcode ?? void 0,
648
892
  name: product.variants.length > 1 ? `${product.title} - ${v.title}` : void 0,
649
893
  price: v.price.amount,
650
894
  priceCurrency: v.price.currency_code,
651
- availability: v.available_for_sale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
895
+ availability: storefront.schemaAvailability(availability.status),
896
+ availabilityStarts: availability.status === "preorder" ? availability.preorder_release_at ?? void 0 : void 0,
652
897
  itemCondition: "https://schema.org/NewCondition",
653
898
  url: product.variants.length > 1 ? withVariant(url, v.id) : url,
654
899
  seller: shop ? { "@type": "Organization", name: shop.name } : void 0
655
- })
656
- );
900
+ });
901
+ });
657
902
  const additionalProperty = Object.entries(product.attributes).map(([name, value]) => ({ "@type": "PropertyValue", name, value }));
658
903
  return compact({
659
904
  "@context": CONTEXT,
@@ -1339,7 +1584,9 @@ function NearestLocationForm({
1339
1584
  }
1340
1585
 
1341
1586
  exports.AddToCartButton = AddToCartButton;
1587
+ exports.AvailabilityBadge = AvailabilityBadge;
1342
1588
  exports.CART_COOKIE_NAME = CART_COOKIE_NAME;
1589
+ exports.CartLineProblem = CartLineProblem;
1343
1590
  exports.CartProvider = CartProvider;
1344
1591
  exports.DEFAULT_IMAGE_WIDTHS = DEFAULT_IMAGE_WIDTHS;
1345
1592
  exports.DirectionsLink = DirectionsLink;
@@ -1352,12 +1599,16 @@ exports.NearestLocationForm = NearestLocationForm;
1352
1599
  exports.OpenStatus = OpenStatus;
1353
1600
  exports.PickupOption = PickupOption;
1354
1601
  exports.ProductPrice = ProductPrice;
1602
+ exports.RestockNotifyForm = RestockNotifyForm;
1355
1603
  exports.SELECTED_LOCATION_PARAM = SELECTED_LOCATION_PARAM;
1356
1604
  exports.SELECTED_LOCATION_STORAGE_KEY = SELECTED_LOCATION_STORAGE_KEY;
1357
1605
  exports.SectionRegistry = SectionRegistry;
1358
1606
  exports.Sections = Sections;
1607
+ exports.SrOnly = SrOnly;
1608
+ exports.StockConflictAlert = StockConflictAlert;
1359
1609
  exports.StorefrontProvider = StorefrontProvider;
1360
1610
  exports.ThemeStyle = ThemeStyle;
1611
+ exports.VariantOptionState = VariantOptionState;
1361
1612
  exports.VariantSelector = VariantSelector;
1362
1613
  exports.breadcrumbJsonLd = breadcrumbJsonLd;
1363
1614
  exports.collectionJsonLd = collectionJsonLd;
@@ -1386,6 +1637,7 @@ exports.themeToCss = themeToCss;
1386
1637
  exports.themeToCssVariables = themeToCssVariables;
1387
1638
  exports.toMerchandise = toMerchandise;
1388
1639
  exports.tokenName = tokenName;
1640
+ exports.useAvailability = useAvailability;
1389
1641
  exports.useCart = useCart;
1390
1642
  exports.useGeolocation = useGeolocation;
1391
1643
  exports.useNearestLocations = useNearestLocations;