@aglyn/plugins-commerce 1.0.0-beta.237 → 1.0.0-beta.239

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.
Files changed (34) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart.d.ts +2 -1
  3. package/src/lib/components/cart.js +2 -1
  4. package/src/lib/components/cart.js.map +1 -1
  5. package/src/lib/components/product-detail.d.ts +76 -0
  6. package/src/lib/components/product-detail.js +258 -52
  7. package/src/lib/components/product-detail.js.map +1 -1
  8. package/src/lib/components/product-grid.d.ts +35 -0
  9. package/src/lib/components/product-grid.js +207 -79
  10. package/src/lib/components/product-grid.js.map +1 -1
  11. package/src/lib/components/product-image-placeholder.d.ts +31 -0
  12. package/src/lib/components/product-image-placeholder.js +72 -0
  13. package/src/lib/components/product-image-placeholder.js.map +1 -0
  14. package/src/lib/components/product-reviews.d.ts +13 -0
  15. package/src/lib/components/product-reviews.js +267 -13
  16. package/src/lib/components/product-reviews.js.map +1 -1
  17. package/src/lib/components/related-products.d.ts +6 -0
  18. package/src/lib/components/related-products.js +73 -20
  19. package/src/lib/components/related-products.js.map +1 -1
  20. package/src/lib/constants/cart-events.d.ts +23 -0
  21. package/src/lib/constants/cart-events.js +23 -0
  22. package/src/lib/constants/cart-events.js.map +1 -0
  23. package/src/lib/server/catalog.d.ts +14 -0
  24. package/src/lib/server/catalog.js +25 -5
  25. package/src/lib/server/catalog.js.map +1 -1
  26. package/src/lib/server/reviews.d.ts +35 -0
  27. package/src/lib/server/reviews.js +120 -1
  28. package/src/lib/server/reviews.js.map +1 -1
  29. package/src/lib/server/site-page-resolver.d.ts +15 -3
  30. package/src/lib/server/site-page-resolver.js +78 -24
  31. package/src/lib/server/site-page-resolver.js.map +1 -1
  32. package/src/lib/utils/add-to-cart.d.ts +35 -0
  33. package/src/lib/utils/add-to-cart.js +53 -0
  34. package/src/lib/utils/add-to-cart.js.map +1 -0
@@ -37,8 +37,29 @@ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
37
37
  import { BUNDLE_ID } from "../constants/bundle-common.js";
38
38
  import { STOREFRONT_CATALOG_DECLARATION, STOREFRONT_CATALOG_HEADERS, STOREFRONT_CATALOG_OPTIONS } from "../constants/storefront-catalog-query.js";
39
39
  import { generatePresetId } from "../utils/generate-preset-id.js";
40
+ import { addToCart } from "../utils/add-to-cart.js";
41
+ import { ProductImagePlaceholder } from "./product-image-placeholder.js";
40
42
  // Component ids are persisted in screen documents; never rename.
41
43
  export const ID = 'product-grid';
44
+ /** What a card's quick add offers for one product. */ export function quickAddAction(item) {
45
+ var _item_variantCount;
46
+ if (item.priceComingSoon || item.soldOut) return null;
47
+ if (item.defaultVariantId) return 'add';
48
+ if (((_item_variantCount = item.variantCount) != null ? _item_variantCount : 0) > 1) return 'options';
49
+ return null;
50
+ }
51
+ /**
52
+ * The chip and tag a browsing grid opens on, from the page's address:
53
+ * `?category=<slug>` and `?tag=<tag>`. Read after hydration only — the
54
+ * server renders the unfiltered first page and has no address to read.
55
+ */ export function browseParamsFromSearch(search) {
56
+ var _params_get, _params_get1;
57
+ const params = new URLSearchParams(search);
58
+ return {
59
+ categorySlug: ((_params_get = params.get('category')) != null ? _params_get : '').trim(),
60
+ tag: ((_params_get1 = params.get('tag')) != null ? _params_get1 : '').trim()
61
+ };
62
+ }
42
63
  /** What a card says where a product listed before it has a price would show one (AGL-3676). */ const PRICE_COMING_SOON_LABEL = 'Price coming soon';
43
64
  const SAMPLE_ITEMS = [
44
65
  {
@@ -141,7 +162,7 @@ function priceLabel(item) {
141
162
  */ const ProductGrid = /*#__PURE__*/ forwardRef((props, ref)=>{
142
163
  var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
143
164
  var _site_pageData_commerce_grids, _site_pageData_commerce, _site_pageData;
144
- const { source, collectionId, collectionSlug: collectionSlugProp, categoryId, categorySlug, tag, sort: sortProp, columns, maxItems, emptyText, emptyTitle, emptyActionLabel, emptyActionHref, showFilters, showSearch, showCategories, showSort, showTypeFilter, showPriceFilter, pageSize, cardStyle } = props, rest = _object_without_properties_loose(props, [
165
+ const { source, collectionId, collectionSlug: collectionSlugProp, categoryId, categorySlug, tag, sort: sortProp, columns, maxItems, emptyText, emptyTitle, emptyActionLabel, emptyActionHref, showFilters, showSearch, showCategories, showSort, showTypeFilter, showPriceFilter, pageSize, cardStyle, quickAdd } = props, rest = _object_without_properties_loose(props, [
145
166
  "source",
146
167
  "collectionId",
147
168
  "collectionSlug",
@@ -162,7 +183,8 @@ function priceLabel(item) {
162
183
  "showTypeFilter",
163
184
  "showPriceFilter",
164
185
  "pageSize",
165
- "cardStyle"
186
+ "cardStyle",
187
+ "quickAdd"
166
188
  ]);
167
189
  const photoCards = cardStyle === 'photo';
168
190
  // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
@@ -170,7 +192,11 @@ function priceLabel(item) {
170
192
  props['sx']
171
193
  ];
172
194
  const site = Aglyn.useSite();
195
+ const siteFetch = Aglyn.useSiteFetch();
173
196
  const { hostId } = site;
197
+ // Quick add (AGL-3676 follow-up): which card is adding, which just did.
198
+ const [quickAdding, setQuickAdding] = useState('');
199
+ const [quickAdded, setQuickAdded] = useState('');
174
200
  // Server-rendered first page (AGL-659). The site-page enricher ran this
175
201
  // grid's own query on the server and keyed the result by node id — two
176
202
  // grids on a page have different queries, so an unkeyed seed would put
@@ -208,6 +234,29 @@ function priceLabel(item) {
208
234
  const collectionSlug = source === 'collection' ? collectionSlugProp || collectionSlugFromLocation() : collectionSlugProp;
209
235
  const pageLimit = pageSize && Number(pageSize) > 0 ? Math.floor(Number(pageSize)) : 0;
210
236
  const pinnedCategoryId = source === 'category' ? categoryId != null ? categoryId : '' : '';
237
+ // A browsing grid opens on the address's `?category=` / `?tag=`: a shop
238
+ // page linked as /shop?category=candles shows that chip picked. Read in
239
+ // an effect, so the server and the first client render agree.
240
+ const [urlCategorySlug, setUrlCategorySlug] = useState('');
241
+ useEffect(()=>{
242
+ if (!hostId || typeof window === 'undefined') return;
243
+ const asked = browseParamsFromSearch(window.location.search);
244
+ if (showCategories && asked.categorySlug) setUrlCategorySlug(asked.categorySlug);
245
+ if (showFilters && asked.tag) setActiveTag(asked.tag);
246
+ // Once, on arrival: later chip picks are the visitor's.
247
+ // eslint-disable-next-line react-hooks/exhaustive-deps
248
+ }, [
249
+ hostId
250
+ ]);
251
+ useEffect(()=>{
252
+ if (!urlCategorySlug || !categories.length) return;
253
+ const match = categories.find((category)=>category.slug === urlCategorySlug);
254
+ if (match) setActiveCategoryId(match.id);
255
+ setUrlCategorySlug('');
256
+ }, [
257
+ urlCategorySlug,
258
+ categories
259
+ ]);
211
260
  // Debounce keystrokes so large catalogs aren't fetched per letter.
212
261
  useEffect(()=>{
213
262
  const timer = setTimeout(()=>setQuery(searchInput.trim()), 300);
@@ -663,90 +712,118 @@ function priceLabel(item) {
663
712
  const imageUrl = Aglyn.siteRelativeMediaSrc(item.imageUrl, {
664
713
  hostId
665
714
  });
666
- return /*#__PURE__*/ _jsx(Card, {
715
+ return /*#__PURE__*/ _jsxs(Card, {
667
716
  variant: photoCards ? 'elevation' : 'outlined',
668
717
  elevation: 0,
669
718
  sx: photoCards ? {
670
719
  bgcolor: 'transparent',
671
720
  overflow: 'visible'
672
721
  } : undefined,
673
- children: /*#__PURE__*/ _jsxs(CardActionArea, {
674
- href: hostId ? `/products/${item.slug}` : undefined,
675
- disabled: !hostId,
676
- sx: photoCards ? {
677
- borderRadius: 2
678
- } : undefined,
679
- children: [
680
- imageUrl ? /*#__PURE__*/ _jsx(CardMedia, _extends({
681
- component: "img",
682
- image: imageUrl,
683
- alt: item.name,
684
- sx: photoCards ? {
685
- aspectRatio: '4 / 5',
686
- objectFit: 'cover',
687
- borderRadius: 2,
688
- // A storefront's photo answers the pointer (AGL-3676).
689
- transition: 'opacity 200ms ease',
690
- '.MuiCardActionArea-root:hover &': {
691
- opacity: 0.88
722
+ children: [
723
+ /*#__PURE__*/ _jsxs(CardActionArea, {
724
+ href: hostId ? `/products/${item.slug}` : undefined,
725
+ disabled: !hostId,
726
+ sx: photoCards ? {
727
+ borderRadius: 2
728
+ } : undefined,
729
+ children: [
730
+ imageUrl ? /*#__PURE__*/ _jsx(CardMedia, _extends({
731
+ component: "img",
732
+ image: imageUrl,
733
+ alt: item.name,
734
+ sx: photoCards ? {
735
+ aspectRatio: '4 / 5',
736
+ objectFit: 'cover',
737
+ borderRadius: 2,
738
+ // A storefront's photo answers the pointer (AGL-3676).
739
+ transition: 'opacity 200ms ease',
740
+ '.MuiCardActionArea-root:hover &': {
741
+ opacity: 0.88
742
+ }
743
+ } : {
744
+ height: 160,
745
+ objectFit: 'cover'
692
746
  }
693
- } : {
694
- height: 160,
695
- objectFit: 'cover'
696
- }
697
- }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES)) : /*#__PURE__*/ _jsx(Box, {
698
- sx: photoCards ? {
699
- aspectRatio: '4 / 5',
700
- bgcolor: 'action.hover',
701
- borderRadius: 2
702
- } : {
703
- height: 160,
704
- bgcolor: 'action.hover'
705
- }
706
- }),
707
- /*#__PURE__*/ _jsxs(CardContent, {
708
- sx: photoCards ? {
709
- px: 0.5,
710
- pt: 1.5,
711
- pb: 1
712
- } : undefined,
713
- children: [
714
- /*#__PURE__*/ _jsx(Typography, {
715
- variant: photoCards ? 'subtitle1' : 'subtitle2',
716
- noWrap: true,
717
- children: item.name
718
- }),
719
- /*#__PURE__*/ _jsxs(Box, {
720
- sx: {
721
- display: 'flex',
722
- gap: 1,
723
- alignItems: 'center'
724
- },
725
- children: [
726
- /*#__PURE__*/ _jsx(Typography, {
727
- variant: "body2",
728
- color: "text.secondary",
729
- children: priceLabel(item)
730
- }),
731
- item.compareAtPriceUsd ? /*#__PURE__*/ _jsx(Typography, {
732
- variant: "caption",
733
- color: "text.disabled",
734
- sx: {
735
- textDecoration: 'line-through'
736
- },
737
- children: `$${item.compareAtPriceUsd}`
738
- }) : null,
739
- item.soldOut ? /*#__PURE__*/ _jsx(Chip, {
740
- label: "Sold out",
741
- size: "small",
742
- variant: "outlined"
743
- }) : null
744
- ]
745
- })
746
- ]
747
- })
748
- ]
749
- })
747
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES)) : // Never a blank tile (AGL-3676 follow-up).
748
+ /*#__PURE__*/ _jsx(ProductImagePlaceholder, {
749
+ name: item.name,
750
+ sx: photoCards ? {
751
+ aspectRatio: '4 / 5',
752
+ borderRadius: 2
753
+ } : {
754
+ height: 160
755
+ }
756
+ }),
757
+ /*#__PURE__*/ _jsxs(CardContent, {
758
+ sx: photoCards ? {
759
+ px: 0.5,
760
+ pt: 1.5,
761
+ pb: 1
762
+ } : undefined,
763
+ children: [
764
+ /*#__PURE__*/ _jsx(Typography, {
765
+ variant: photoCards ? 'subtitle1' : 'subtitle2',
766
+ noWrap: true,
767
+ children: item.name
768
+ }),
769
+ /*#__PURE__*/ _jsxs(Box, {
770
+ sx: {
771
+ display: 'flex',
772
+ gap: 1,
773
+ alignItems: 'center'
774
+ },
775
+ children: [
776
+ /*#__PURE__*/ _jsx(Typography, {
777
+ variant: "body2",
778
+ color: "text.secondary",
779
+ children: priceLabel(item)
780
+ }),
781
+ item.compareAtPriceUsd ? /*#__PURE__*/ _jsx(Typography, {
782
+ variant: "caption",
783
+ color: "text.disabled",
784
+ sx: {
785
+ textDecoration: 'line-through'
786
+ },
787
+ children: `$${item.compareAtPriceUsd}`
788
+ }) : null,
789
+ item.soldOut ? /*#__PURE__*/ _jsx(Chip, {
790
+ label: "Sold out",
791
+ size: "small",
792
+ variant: "outlined"
793
+ }) : null
794
+ ]
795
+ })
796
+ ]
797
+ })
798
+ ]
799
+ }),
800
+ quickAdd ? /*#__PURE__*/ _jsx(QuickAddButton, {
801
+ item: item,
802
+ inert: !hostId,
803
+ busy: quickAdding === item.id,
804
+ added: quickAdded === item.id,
805
+ photoCards: photoCards,
806
+ onAdd: async ()=>{
807
+ if (!hostId || !item.defaultVariantId || quickAdding) return;
808
+ setQuickAdding(item.id);
809
+ const ok = await addToCart({
810
+ hostId,
811
+ siteFetch,
812
+ productId: item.id,
813
+ variantId: item.defaultVariantId,
814
+ quantity: 1,
815
+ item: {
816
+ item_id: item.id,
817
+ item_name: item.name,
818
+ price: item.priceUsd,
819
+ quantity: 1
820
+ }
821
+ });
822
+ setQuickAdding('');
823
+ if (ok) setQuickAdded(item.id);
824
+ }
825
+ }) : null
826
+ ]
750
827
  }, item.id);
751
828
  })
752
829
  });
@@ -834,6 +911,50 @@ function priceLabel(item) {
834
911
  ]
835
912
  }));
836
913
  });
914
+ /**
915
+ * A card's quick add: outside the card's link, so a click never follows it.
916
+ * Sample cards on the canvas show the button inert.
917
+ */ function QuickAddButton({ item, inert, busy, added, photoCards, onAdd }) {
918
+ const action = inert ? 'add' : quickAddAction(item);
919
+ if (!action) return null;
920
+ const sx = photoCards ? {
921
+ mt: 0.5,
922
+ mx: 0.5,
923
+ mb: 1
924
+ } : {
925
+ mx: 2,
926
+ mb: 2
927
+ };
928
+ if (action === 'options') {
929
+ return /*#__PURE__*/ _jsx(Box, {
930
+ sx: sx,
931
+ children: /*#__PURE__*/ _jsx(Button, {
932
+ size: "small",
933
+ variant: "outlined",
934
+ fullWidth: true,
935
+ href: `/products/${item.slug}`,
936
+ onClick: (event)=>event.stopPropagation(),
937
+ children: 'Choose options'
938
+ })
939
+ });
940
+ }
941
+ return /*#__PURE__*/ _jsx(Box, {
942
+ sx: sx,
943
+ children: /*#__PURE__*/ _jsx(Button, {
944
+ size: "small",
945
+ variant: "outlined",
946
+ fullWidth: true,
947
+ disabled: inert || busy,
948
+ "aria-label": `Add ${item.name} to cart`,
949
+ onClick: (event)=>{
950
+ event.preventDefault();
951
+ event.stopPropagation();
952
+ onAdd();
953
+ },
954
+ children: added ? 'Added ✓' : busy ? 'Adding…' : 'Add to cart'
955
+ })
956
+ });
957
+ }
837
958
  ProductGrid.displayName = 'AglynProductGrid';
838
959
  export const schema = {
839
960
  $id: ID,
@@ -1025,6 +1146,13 @@ export const schema = {
1025
1146
  value: 'photo'
1026
1147
  }
1027
1148
  ]
1149
+ },
1150
+ {
1151
+ // Off by default, so no existing grid changes shape on deploy.
1152
+ name: 'quickAdd',
1153
+ label: 'Quick add to cart',
1154
+ description: 'An Add to cart button on each card for a product with one variant; ' + '"Choose options" for one with several.',
1155
+ component: Aglyn.FieldComponentType.CHECKBOX
1028
1156
  }
1029
1157
  ]
1030
1158
  };