@doswiftly/cli 0.1.18 → 0.1.19

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 (210) hide show
  1. package/README.md +23 -323
  2. package/dist/commands/check.js +1 -1
  3. package/dist/commands/check.js.map +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +39 -20
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/doctor.js +3 -3
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/init.js +4 -4
  10. package/dist/commands/sdk.js +5 -5
  11. package/dist/commands/sdk.js.map +1 -1
  12. package/dist/commands/template.js +4 -4
  13. package/dist/commands/template.js.map +1 -1
  14. package/dist/commands/types.js +5 -5
  15. package/dist/commands/types.js.map +1 -1
  16. package/dist/commands/verify.js +2 -2
  17. package/dist/commands/verify.js.map +1 -1
  18. package/dist/lib/package-manager.d.ts +1 -1
  19. package/dist/lib/package-manager.js +1 -1
  20. package/package.json +1 -1
  21. package/templates/storefront-nextjs/README.md +16 -12
  22. package/templates/storefront-nextjs/app/account/orders/page.tsx +2 -2
  23. package/templates/storefront-nextjs/app/account/page.tsx +2 -2
  24. package/templates/storefront-nextjs/app/auth/login/page.tsx +1 -1
  25. package/templates/storefront-nextjs/app/auth/register/page.tsx +1 -1
  26. package/templates/storefront-nextjs/app/cart/page.tsx +1 -1
  27. package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +2 -2
  28. package/templates/storefront-nextjs/app/categories/page.tsx +1 -1
  29. package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +1 -1
  30. package/templates/storefront-nextjs/app/collections/page.tsx +1 -1
  31. package/templates/storefront-nextjs/app/page.tsx +1 -1
  32. package/templates/storefront-nextjs/app/products/[slug]/page.tsx +1 -1
  33. package/templates/storefront-nextjs/app/products/page.tsx +2 -2
  34. package/templates/storefront-nextjs/app/search/page.tsx +1 -1
  35. package/templates/storefront-nextjs/components/auth/auth-guard.tsx +1 -1
  36. package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +1 -1
  37. package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +1 -1
  38. package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +2 -2
  39. package/templates/storefront-nextjs/components/commerce/product-filters.tsx +1 -1
  40. package/templates/storefront-nextjs/components/commerce/product-price.tsx +1 -1
  41. package/templates/storefront-nextjs/components/commerce/search-input.tsx +1 -1
  42. package/templates/storefront-nextjs/components/commerce/sort-select.tsx +1 -1
  43. package/templates/storefront-nextjs/components/providers.tsx +1 -1
  44. package/templates/storefront-nextjs/lib/currency.tsx +3 -3
  45. package/templates/storefront-nextjs/lib/format.ts +1 -1
  46. package/templates/storefront-nextjs/lib/graphql-queries.ts +3 -3
  47. package/templates/storefront-nextjs/package.dev.json +1 -1
  48. package/templates/storefront-nextjs/package.json +1 -1
  49. package/templates/storefront-nextjs/package.json.template +1 -1
  50. package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +47 -0
  51. package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +47 -0
  52. package/templates/storefront-nextjs-shadcn/CLAUDE.md +148 -35
  53. package/templates/storefront-nextjs-shadcn/README.md +29 -162
  54. package/templates/storefront-nextjs-shadcn/app/account/addresses/page.tsx +98 -91
  55. package/templates/storefront-nextjs-shadcn/app/account/error.tsx +43 -0
  56. package/templates/storefront-nextjs-shadcn/app/account/loading.tsx +19 -0
  57. package/templates/storefront-nextjs-shadcn/app/account/loyalty/page.tsx +53 -162
  58. package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/loading.tsx +60 -0
  59. package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/page.tsx +36 -47
  60. package/templates/storefront-nextjs-shadcn/app/account/orders/page.tsx +46 -29
  61. package/templates/storefront-nextjs-shadcn/app/account/page.tsx +8 -5
  62. package/templates/storefront-nextjs-shadcn/app/account/settings/page.tsx +108 -71
  63. package/templates/storefront-nextjs-shadcn/app/api/auth/clear-token/route.ts +2 -86
  64. package/templates/storefront-nextjs-shadcn/app/api/auth/set-token/route.ts +2 -124
  65. package/templates/storefront-nextjs-shadcn/app/auth/forgot-password/page.tsx +10 -5
  66. package/templates/storefront-nextjs-shadcn/app/blog/[slug]/loading.tsx +17 -0
  67. package/templates/storefront-nextjs-shadcn/app/blog/[slug]/page.tsx +43 -2
  68. package/templates/storefront-nextjs-shadcn/app/blog/loading.tsx +19 -0
  69. package/templates/storefront-nextjs-shadcn/app/brands/page.tsx +2 -1
  70. package/templates/storefront-nextjs-shadcn/app/cart/loading.tsx +26 -0
  71. package/templates/storefront-nextjs-shadcn/app/cart/page.tsx +6 -3
  72. package/templates/storefront-nextjs-shadcn/app/categories/[slug]/category-products-client.tsx +56 -0
  73. package/templates/storefront-nextjs-shadcn/app/categories/[slug]/loading.tsx +32 -0
  74. package/templates/storefront-nextjs-shadcn/app/categories/[slug]/page.tsx +76 -59
  75. package/templates/storefront-nextjs-shadcn/app/categories/page.tsx +8 -4
  76. package/templates/storefront-nextjs-shadcn/app/checkout/error.tsx +43 -0
  77. package/templates/storefront-nextjs-shadcn/app/checkout/loading.tsx +31 -0
  78. package/templates/storefront-nextjs-shadcn/app/checkout/page.tsx +116 -79
  79. package/templates/storefront-nextjs-shadcn/app/collections/[handle]/loading.tsx +19 -0
  80. package/templates/storefront-nextjs-shadcn/app/collections/[handle]/page.tsx +1 -1
  81. package/templates/storefront-nextjs-shadcn/app/collections/loading.tsx +18 -0
  82. package/templates/storefront-nextjs-shadcn/app/collections/page.tsx +7 -4
  83. package/templates/storefront-nextjs-shadcn/app/global-error.tsx +117 -0
  84. package/templates/storefront-nextjs-shadcn/app/globals.css +8 -0
  85. package/templates/storefront-nextjs-shadcn/app/layout.tsx +46 -11
  86. package/templates/storefront-nextjs-shadcn/app/products/[slug]/error.tsx +43 -0
  87. package/templates/storefront-nextjs-shadcn/app/products/[slug]/loading.tsx +29 -0
  88. package/templates/storefront-nextjs-shadcn/app/products/[slug]/page.tsx +6 -6
  89. package/templates/storefront-nextjs-shadcn/app/products/[slug]/product-client.tsx +15 -61
  90. package/templates/storefront-nextjs-shadcn/app/products/loading.tsx +32 -0
  91. package/templates/storefront-nextjs-shadcn/app/products/products-client.tsx +405 -151
  92. package/templates/storefront-nextjs-shadcn/app/search/loading.tsx +18 -0
  93. package/templates/storefront-nextjs-shadcn/app/wishlist/page.tsx +8 -5
  94. package/templates/storefront-nextjs-shadcn/codegen.ts +48 -31
  95. package/templates/storefront-nextjs-shadcn/components/account/customer-info.fragment.graphql +36 -0
  96. package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +3 -1
  97. package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +26 -24
  98. package/templates/storefront-nextjs-shadcn/components/account/order-summary.fragment.graphql +36 -0
  99. package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +9 -9
  100. package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +11 -37
  101. package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +37 -23
  102. package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +4 -3
  103. package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +8 -5
  104. package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +1 -1
  105. package/templates/storefront-nextjs-shadcn/components/cart/cart-line.fragment.graphql +53 -0
  106. package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +1 -1
  107. package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +22 -7
  108. package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +2 -2
  109. package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +1 -1
  110. package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +2 -2
  111. package/templates/storefront-nextjs-shadcn/components/common/price-display.tsx +35 -11
  112. package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +1 -1
  113. package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +3 -3
  114. package/templates/storefront-nextjs-shadcn/components/filters/range-slider-filter.tsx +5 -5
  115. package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +2 -2
  116. package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +2 -1
  117. package/templates/storefront-nextjs-shadcn/components/home/collection-card.fragment.graphql +21 -0
  118. package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +2 -12
  119. package/templates/storefront-nextjs-shadcn/components/home/index.ts +0 -1
  120. package/templates/storefront-nextjs-shadcn/components/hydrated.tsx +24 -0
  121. package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +4 -4
  122. package/templates/storefront-nextjs-shadcn/components/layout/category-node.fragment.graphql +22 -0
  123. package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +2 -2
  124. package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +33 -23
  125. package/templates/storefront-nextjs-shadcn/components/loyalty/points-balance.tsx +2 -11
  126. package/templates/storefront-nextjs-shadcn/components/loyalty/points-history.tsx +8 -25
  127. package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +10 -19
  128. package/templates/storefront-nextjs-shadcn/components/loyalty/rewards-catalog.tsx +17 -41
  129. package/templates/storefront-nextjs-shadcn/components/loyalty/tier-progress.tsx +2 -29
  130. package/templates/storefront-nextjs-shadcn/components/order/index.ts +6 -1
  131. package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +3 -1
  132. package/templates/storefront-nextjs-shadcn/components/product/filter-active-pills.tsx +69 -0
  133. package/templates/storefront-nextjs-shadcn/components/product/filter-mobile-sheet.tsx +84 -0
  134. package/templates/storefront-nextjs-shadcn/components/product/filter-price-range.tsx +138 -0
  135. package/templates/storefront-nextjs-shadcn/components/product/index.ts +9 -2
  136. package/templates/storefront-nextjs-shadcn/components/product/product-card.fragment.graphql +49 -0
  137. package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +3 -31
  138. package/templates/storefront-nextjs-shadcn/components/product/product-detail.fragment.graphql +52 -0
  139. package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +176 -123
  140. package/templates/storefront-nextjs-shadcn/components/product/product-grid.tsx +3 -5
  141. package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +2 -2
  142. package/templates/storefront-nextjs-shadcn/components/product/product-price.tsx +2 -2
  143. package/templates/storefront-nextjs-shadcn/components/product/product-reviews.tsx +5 -4
  144. package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +19 -7
  145. package/templates/storefront-nextjs-shadcn/components/product/product-variant-selector.tsx +8 -23
  146. package/templates/storefront-nextjs-shadcn/components/product/product-variant.fragment.graphql +51 -0
  147. package/templates/storefront-nextjs-shadcn/components/product/review-card.tsx +1 -1
  148. package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +1 -7
  149. package/templates/storefront-nextjs-shadcn/components/product/savings-display.tsx +17 -2
  150. package/templates/storefront-nextjs-shadcn/components/product/similar-products.tsx +3 -2
  151. package/templates/storefront-nextjs-shadcn/components/providers/index.ts +1 -1
  152. package/templates/storefront-nextjs-shadcn/components/providers/stores-provider.tsx +30 -0
  153. package/templates/storefront-nextjs-shadcn/components/providers/theme-provider.tsx +1 -1
  154. package/templates/storefront-nextjs-shadcn/components/returns/index.ts +2 -2
  155. package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +3 -2
  156. package/templates/storefront-nextjs-shadcn/components/search/search-results.tsx +3 -2
  157. package/templates/storefront-nextjs-shadcn/components/ui/form.tsx +174 -0
  158. package/templates/storefront-nextjs-shadcn/components/ui/index.ts +30 -2
  159. package/templates/storefront-nextjs-shadcn/components/ui/progress.tsx +40 -0
  160. package/templates/storefront-nextjs-shadcn/components/ui/sheet.tsx +107 -0
  161. package/templates/storefront-nextjs-shadcn/components/ui/slider.tsx +33 -0
  162. package/templates/storefront-nextjs-shadcn/components/ui/textarea.tsx +24 -0
  163. package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +3 -1
  164. package/templates/storefront-nextjs-shadcn/generated/graphql.ts +12779 -0
  165. package/templates/storefront-nextjs-shadcn/graphql/custom.example.graphql +159 -0
  166. package/templates/storefront-nextjs-shadcn/hooks/index.ts +2 -0
  167. package/templates/storefront-nextjs-shadcn/hooks/use-auth-sync.ts +42 -0
  168. package/templates/storefront-nextjs-shadcn/hooks/use-auth.ts +17 -295
  169. package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +51 -19
  170. package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +13 -9
  171. package/templates/storefront-nextjs-shadcn/lib/auth/routes.ts +4 -17
  172. package/templates/storefront-nextjs-shadcn/lib/graphql/client.ts +22 -99
  173. package/templates/storefront-nextjs-shadcn/lib/graphql/config.ts +32 -0
  174. package/templates/storefront-nextjs-shadcn/lib/graphql/fragments.ts +34 -0
  175. package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +687 -632
  176. package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +86 -0
  177. package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +131 -182
  178. package/templates/storefront-nextjs-shadcn/lib/graphql/types.ts +62 -0
  179. package/templates/storefront-nextjs-shadcn/lib/theme/theme-config.ts +0 -17
  180. package/templates/storefront-nextjs-shadcn/next-env.d.ts +6 -0
  181. package/templates/storefront-nextjs-shadcn/package.dev.json +1 -3
  182. package/templates/storefront-nextjs-shadcn/package.json +12 -13
  183. package/templates/storefront-nextjs-shadcn/package.json.template +6 -7
  184. package/templates/storefront-nextjs-shadcn/proxy.ts +3 -4
  185. package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +41 -39
  186. package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +64 -75
  187. package/templates/storefront-nextjs-shadcn/stores/wishlist-store.ts +178 -177
  188. package/templates/storefront-nextjs-shadcn/tsconfig.json +23 -5
  189. package/templates/storefront-nextjs-shadcn/CART_INTEGRATION.md +0 -282
  190. package/templates/storefront-nextjs-shadcn/GRAPHQL_DOCUMENT_NAMES.md +0 -190
  191. package/templates/storefront-nextjs-shadcn/GRAPHQL_ERROR_HANDLING.md +0 -263
  192. package/templates/storefront-nextjs-shadcn/GRAPHQL_FIXES_SUMMARY.md +0 -135
  193. package/templates/storefront-nextjs-shadcn/GRAPHQL_INTEGRATION_COMPLETE.md +0 -142
  194. package/templates/storefront-nextjs-shadcn/INTEGRATION_CHECKLIST.md +0 -448
  195. package/templates/storefront-nextjs-shadcn/PRODUCT_DETAIL_PAGE_IMPLEMENTATION.md +0 -307
  196. package/templates/storefront-nextjs-shadcn/THEME_CUSTOMIZATION.md +0 -245
  197. package/templates/storefront-nextjs-shadcn/components/providers/currency-provider.tsx +0 -103
  198. package/templates/storefront-nextjs-shadcn/graphql/collections.example.ts +0 -168
  199. package/templates/storefront-nextjs-shadcn/graphql/products.example.ts +0 -160
  200. package/templates/storefront-nextjs-shadcn/lib/auth/cookies.ts +0 -220
  201. package/templates/storefront-nextjs-shadcn/lib/config.ts +0 -46
  202. package/templates/storefront-nextjs-shadcn/lib/currency/IMPLEMENTATION_SUMMARY.md +0 -254
  203. package/templates/storefront-nextjs-shadcn/lib/currency/README.md +0 -464
  204. package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.test.ts +0 -328
  205. package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.ts +0 -295
  206. package/templates/storefront-nextjs-shadcn/lib/currency/index.ts +0 -27
  207. package/templates/storefront-nextjs-shadcn/lib/format.ts +0 -226
  208. package/templates/storefront-nextjs-shadcn/lib/hooks.ts +0 -30
  209. package/templates/storefront-nextjs-shadcn/stores/auth-store.ts +0 -66
  210. package/templates/storefront-nextjs-shadcn/stores/currency-store.ts +0 -103
@@ -1,191 +1,445 @@
1
1
  "use client";
2
2
 
3
- import { Suspense } from "react";
4
- import { useSearchParams, useRouter } from "next/navigation";
3
+ import { useCallback, useMemo, useTransition } from "react";
4
+ import { useSearchParams, useRouter, usePathname } from "next/navigation";
5
+ import { keepPreviousData } from "@tanstack/react-query";
5
6
  import { ProductGrid } from "@/components/product/product-grid";
6
- import { ProductFilters } from "@/components/product/product-filters";
7
+ import { ProductFilters, type FilterGroup } from "@/components/product/product-filters";
7
8
  import { ProductSort, type SortOption } from "@/components/product/product-sort";
8
- import { Pagination } from "@/components/ui/pagination";
9
+ import {
10
+ FilterActivePills,
11
+ type ActivePill,
12
+ } from "@/components/product/filter-active-pills";
13
+ import { FilterMobileSheet } from "@/components/product/filter-mobile-sheet";
9
14
  import { Skeleton } from "@/components/ui/skeleton";
10
- import { useProducts, useCategories } from "@/lib/graphql/hooks";
15
+ import { Pagination } from "@/components/ui/pagination";
16
+ import { useProducts, useAvailableFilters } from "@/lib/graphql/hooks";
17
+ import type {
18
+ ProductFilterInput,
19
+ AttributeFilterInput,
20
+ } from "@/generated/graphql";
21
+ import { cn } from "@/lib/utils";
11
22
 
23
+ /**
24
+ * ProductsClient — Dynamic product listing with API-driven filters.
25
+ *
26
+ * UX patterns from BigCommerce Catalyst + Saleor Storefront:
27
+ * - useTransition wraps all filter updates (non-blocking UI)
28
+ * - keepPreviousData prevents skeleton flash on filter changes
29
+ * - data-pending CSS opacity dimming during transitions
30
+ * - Active filter pills with individual removal
31
+ * - Mobile sheet for filters (lg:hidden)
32
+ * - { scroll: false } on all URL updates
33
+ *
34
+ * URL State (SEO-friendly, shareable):
35
+ * ?category=id&collection=id
36
+ * &price_min=100&price_max=500
37
+ * &attr_color=red,blue&attr_size=xl
38
+ * &sort=price-low-to-high&page=2&q=search
39
+ */
12
40
  export function ProductsClient() {
13
41
  const searchParams = useSearchParams();
14
42
  const router = useRouter();
15
-
16
- // Parse URL parameters (all lowercase)
43
+ const pathname = usePathname();
44
+ const [isPending, startTransition] = useTransition();
45
+
46
+ // ========== Parse URL State ==========
17
47
  const page = parseInt(searchParams.get("page") || "1", 10);
18
48
  const sort = (searchParams.get("sort") as SortOption) || "relevance";
19
- const categories = searchParams.get("categories")?.split(",").filter(Boolean) || [];
49
+ const searchQuery = searchParams.get("q") || undefined;
50
+ const categoryId = searchParams.get("category") || undefined;
51
+ const collectionId = searchParams.get("collection") || undefined;
20
52
  const priceMin = searchParams.get("price_min");
21
53
  const priceMax = searchParams.get("price_max");
22
-
54
+
23
55
  const limit = 20;
24
56
 
25
- // Fetch categories for filter sidebar
26
- const { data: categoriesData } = useCategories();
27
- const allCategories = categoriesData?.categories ?? [];
28
-
29
- // Build GraphQL query string for filters
30
- let queryString = "";
31
- if (categories.length > 0) {
32
- // Use category handles in query
33
- queryString = categories.map(cat => `category:${cat}`).join(" OR ");
34
- }
35
-
36
- // Fetch products using GraphQL
37
- // Backend should normalize sort values (e.g., "price-asc" → PRICE + reverse: false)
38
- const { data, isLoading, error } = useProducts({
39
- first: limit,
40
- query: queryString || undefined,
41
- sortKey: sort as any, // Backend normalizes this
42
- });
57
+ // Parse dynamic attribute filters: attr_color=red,blue → { attributeId: 'color', values: ['red', 'blue'] }
58
+ const attributeFilters = useMemo(() => {
59
+ const filters: AttributeFilterInput[] = [];
60
+ searchParams.forEach((value: string, key: string) => {
61
+ if (key.startsWith("attr_")) {
62
+ const attributeId = key.slice(5);
63
+ const values = value.split(",").filter(Boolean);
64
+ if (values.length > 0) {
65
+ filters.push({ attributeId, values });
66
+ }
67
+ }
68
+ });
69
+ return filters;
70
+ }, [searchParams]);
71
+
72
+ // ========== Build GraphQL Filter Input ==========
73
+ const filters: ProductFilterInput = useMemo(() => {
74
+ const f: ProductFilterInput = {};
75
+ if (categoryId) f.categoryId = categoryId;
76
+ if (collectionId) f.collectionId = collectionId;
77
+ if (priceMin) f.minPrice = parseFloat(priceMin);
78
+ if (priceMax) f.maxPrice = parseFloat(priceMax);
79
+ if (attributeFilters.length > 0) f.attributes = attributeFilters;
80
+ return f;
81
+ }, [categoryId, collectionId, priceMin, priceMax, attributeFilters]);
82
+
83
+ // ========== Data Fetching (keepPreviousData = no flash) ==========
84
+ const {
85
+ data,
86
+ isLoading: isProductsLoading,
87
+ isFetching: isProductsFetching,
88
+ } = useProducts(
89
+ {
90
+ first: limit,
91
+ query: searchQuery,
92
+ sortKey: sort,
93
+ filters: Object.keys(filters).length > 0 ? filters : undefined,
94
+ },
95
+ { placeholderData: keepPreviousData }
96
+ );
97
+
98
+ const {
99
+ data: filtersData,
100
+ isLoading: isFiltersLoading,
101
+ } = useAvailableFilters(
102
+ {
103
+ categoryId,
104
+ collectionId,
105
+ searchQuery,
106
+ },
107
+ { placeholderData: keepPreviousData }
108
+ );
43
109
 
44
110
  const products = data?.products ?? [];
45
111
  const totalCount = data?.totalCount ?? 0;
46
112
  const totalPages = Math.ceil(totalCount / limit);
113
+ const availableFilters = filtersData?.availableFilters;
47
114
 
48
- // Build selected filters object
49
- const selectedFilters: Record<string, any> = {};
50
- if (categories.length > 0) {
51
- selectedFilters.categories = categories;
52
- }
53
- if (priceMin) {
54
- selectedFilters.price_min = priceMin;
55
- }
56
- if (priceMax) {
57
- selectedFilters.price_max = priceMax;
58
- }
59
-
60
- // Update URL with new filters
61
- const updateFilters = (updates: Record<string, any>) => {
62
- const newParams = new URLSearchParams(searchParams.toString());
63
-
64
- Object.entries(updates).forEach(([key, value]) => {
65
- if (value === null || value === undefined || value === "") {
66
- newParams.delete(key);
67
- } else if (Array.isArray(value)) {
68
- if (value.length > 0) {
69
- newParams.set(key, value.join(","));
70
- } else {
71
- newParams.delete(key);
115
+ // Show dimming when transition or fetch is in progress (but data exists)
116
+ const showDimming = (isPending || isProductsFetching) && products.length > 0;
117
+ // Show skeleton only on initial load (no data yet)
118
+ const showSkeleton = isProductsLoading && products.length === 0;
119
+
120
+ // ========== URL Update (all writes go through here) ==========
121
+ const updateUrl = useCallback(
122
+ (updates: Record<string, string | null>, resetPage = true) => {
123
+ startTransition(() => {
124
+ const newParams = new URLSearchParams(searchParams.toString());
125
+
126
+ for (const [key, value] of Object.entries(updates)) {
127
+ if (value === null || value === "") {
128
+ newParams.delete(key);
129
+ } else {
130
+ newParams.set(key, value);
131
+ }
132
+ }
133
+
134
+ // Reset to page 1 when filters change (not when paginating)
135
+ if (resetPage && !("page" in updates)) {
136
+ newParams.delete("page");
72
137
  }
73
- } else {
74
- newParams.set(key, value.toString());
138
+
139
+ const qs = newParams.toString();
140
+ router.push(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
141
+ });
142
+ },
143
+ [searchParams, router, pathname, startTransition]
144
+ );
145
+
146
+ // ========== Filter Groups (from API) ==========
147
+ const filterGroups: FilterGroup[] = useMemo(() => {
148
+ const groups: FilterGroup[] = [];
149
+
150
+ // Categories
151
+ if (availableFilters?.categories && availableFilters.categories.length > 0) {
152
+ groups.push({
153
+ id: "category",
154
+ label: "Categories",
155
+ type: "checkbox",
156
+ options: availableFilters.categories.map((cat) => ({
157
+ label: cat.name,
158
+ value: cat.id,
159
+ count: cat.productCount,
160
+ })),
161
+ });
162
+ }
163
+
164
+ // Price range (from API — real min/max scoped to context)
165
+ if (availableFilters?.priceRange) {
166
+ const minPrice = parseFloat(availableFilters.priceRange.min.amount);
167
+ const maxPrice = parseFloat(availableFilters.priceRange.max.amount);
168
+ if (minPrice !== maxPrice) {
169
+ groups.push({
170
+ id: "price",
171
+ label: "Price",
172
+ type: "range",
173
+ min: Math.floor(minPrice),
174
+ max: Math.ceil(maxPrice),
175
+ currency: availableFilters.priceRange.min.currencyCode,
176
+ });
75
177
  }
76
- });
77
-
78
- // Reset to page 1 when filters change
79
- newParams.set("page", "1");
80
- router.push(`/products?${newParams.toString()}`);
81
- };
178
+ }
82
179
 
83
- // Handle filter changes
84
- const handleFilterChange = (filterId: string, value: any) => {
85
- if (filterId === "categories") {
86
- // Toggle category in array
87
- const newCategories = categories.includes(value)
88
- ? categories.filter(c => c !== value)
89
- : [...categories, value];
90
- updateFilters({ categories: newCategories });
91
- } else if (filterId === "price_min") {
92
- updateFilters({ price_min: value });
93
- } else if (filterId === "price_max") {
94
- updateFilters({ price_max: value });
180
+ // Dynamic attribute filters
181
+ if (availableFilters?.attributes) {
182
+ for (const attr of availableFilters.attributes) {
183
+ if (attr.filterValues && attr.filterValues.length > 0) {
184
+ const isColor = attr.type === "COLOR";
185
+ groups.push({
186
+ id: `attr_${attr.handle}`,
187
+ label: attr.name,
188
+ type: isColor ? "color" : "checkbox",
189
+ options: attr.filterValues.map((fv) => ({
190
+ label: fv.label,
191
+ value: fv.value,
192
+ count: fv.productCount,
193
+ colorHex: fv.swatch?.colorHex || undefined,
194
+ })),
195
+ });
196
+ } else if (
197
+ attr.rangeBounds &&
198
+ attr.rangeBounds.min != null &&
199
+ attr.rangeBounds.max != null
200
+ ) {
201
+ groups.push({
202
+ id: `attr_${attr.handle}`,
203
+ label: attr.name,
204
+ type: "range",
205
+ min: attr.rangeBounds.min,
206
+ max: attr.rangeBounds.max,
207
+ });
208
+ }
209
+ }
95
210
  }
96
- };
97
211
 
98
- // Build filter options from categories
99
- const filterOptions = [
100
- {
101
- id: "categories",
102
- label: "Categories",
103
- type: "checkbox" as const,
104
- options: allCategories.map((category: any) => ({
105
- label: category.name,
106
- value: category.slug,
107
- count: category.productCount || 0,
108
- })),
212
+ return groups;
213
+ }, [availableFilters]);
214
+
215
+ // ========== Selected Filters (from URL) ==========
216
+ const selectedFilters: Record<string, string[]> = useMemo(() => {
217
+ const sel: Record<string, string[]> = {};
218
+
219
+ if (categoryId) {
220
+ sel["category"] = categoryId.split(",").filter(Boolean);
221
+ }
222
+ if (priceMin || priceMax) {
223
+ sel["price"] = [`${priceMin || 0}-${priceMax || 999999}`];
224
+ }
225
+ for (const af of attributeFilters) {
226
+ sel[`attr_${af.attributeId}`] = af.values || [];
227
+ }
228
+
229
+ return sel;
230
+ }, [categoryId, priceMin, priceMax, attributeFilters]);
231
+
232
+ // ========== Active Filter Pills ==========
233
+ const activePills: ActivePill[] = useMemo(() => {
234
+ const pills: ActivePill[] = [];
235
+
236
+ // Category pills
237
+ if (categoryId && availableFilters?.categories) {
238
+ for (const catId of categoryId.split(",").filter(Boolean)) {
239
+ const cat = availableFilters.categories.find((c) => c.id === catId);
240
+ pills.push({
241
+ filterId: "category",
242
+ label: "Category",
243
+ value: catId,
244
+ displayValue: cat?.name || catId,
245
+ });
246
+ }
247
+ }
248
+
249
+ // Price pill
250
+ if (priceMin || priceMax) {
251
+ const currency =
252
+ availableFilters?.priceRange?.min.currencyCode || "PLN";
253
+ pills.push({
254
+ filterId: "price",
255
+ label: "Price",
256
+ value: `${priceMin || 0}-${priceMax || "∞"}`,
257
+ displayValue: `${priceMin || 0} — ${priceMax || "∞"} ${currency}`,
258
+ });
259
+ }
260
+
261
+ // Attribute pills
262
+ for (const af of attributeFilters) {
263
+ const attrDef = availableFilters?.attributes?.find(
264
+ (a) => a.handle === af.attributeId
265
+ );
266
+ for (const val of af.values || []) {
267
+ const fv = attrDef?.filterValues?.find((v) => v.value === val);
268
+ pills.push({
269
+ filterId: `attr_${af.attributeId}`,
270
+ label: attrDef?.name || af.attributeId,
271
+ value: val,
272
+ displayValue: fv?.label || val,
273
+ });
274
+ }
275
+ }
276
+
277
+ return pills;
278
+ }, [categoryId, priceMin, priceMax, attributeFilters, availableFilters]);
279
+
280
+ const activeFilterCount = activePills.length;
281
+
282
+ // ========== Event Handlers ==========
283
+ const handleFilterChange = useCallback(
284
+ (filterId: string, values: string[]) => {
285
+ if (filterId === "category") {
286
+ updateUrl({ category: values.length > 0 ? values.join(",") : null });
287
+ } else if (filterId === "price") {
288
+ if (values.length > 0) {
289
+ const [min, max] = values[0].split("-");
290
+ updateUrl({
291
+ price_min: min && min !== "0" ? min : null,
292
+ price_max: max && max !== "999999" ? max : null,
293
+ });
294
+ } else {
295
+ updateUrl({ price_min: null, price_max: null });
296
+ }
297
+ } else if (filterId.startsWith("attr_")) {
298
+ updateUrl({ [filterId]: values.length > 0 ? values.join(",") : null });
299
+ }
109
300
  },
110
- {
111
- id: "price",
112
- label: "Price Range",
113
- type: "range" as const,
114
- min: 0,
115
- max: 1000,
301
+ [updateUrl]
302
+ );
303
+
304
+ const handleRemovePill = useCallback(
305
+ (filterId: string, value: string) => {
306
+ if (filterId === "category") {
307
+ const remaining = (categoryId || "")
308
+ .split(",")
309
+ .filter((id) => id !== value);
310
+ updateUrl({
311
+ category: remaining.length > 0 ? remaining.join(",") : null,
312
+ });
313
+ } else if (filterId === "price") {
314
+ updateUrl({ price_min: null, price_max: null });
315
+ } else if (filterId.startsWith("attr_")) {
316
+ const attrId = filterId.slice(5);
317
+ const current = searchParams.get(filterId)?.split(",") || [];
318
+ const remaining = current.filter((v) => v !== value);
319
+ updateUrl({
320
+ [filterId]: remaining.length > 0 ? remaining.join(",") : null,
321
+ });
322
+ }
323
+ },
324
+ [categoryId, searchParams, updateUrl]
325
+ );
326
+
327
+ const handleClearAll = useCallback(() => {
328
+ startTransition(() => {
329
+ router.push(pathname, { scroll: false });
330
+ });
331
+ }, [router, pathname, startTransition]);
332
+
333
+ const handleSortChange = useCallback(
334
+ (newSort: SortOption) => {
335
+ updateUrl({ sort: newSort !== "relevance" ? newSort : null });
336
+ },
337
+ [updateUrl]
338
+ );
339
+
340
+ const handlePageChange = useCallback(
341
+ (newPage: number) => {
342
+ updateUrl(
343
+ { page: newPage > 1 ? newPage.toString() : null },
344
+ false
345
+ );
116
346
  },
117
- ];
347
+ [updateUrl]
348
+ );
118
349
 
119
- return (
120
- <div className="flex flex-col gap-8 lg:flex-row">
121
- {/* Sidebar Filters */}
122
- <aside className="w-full lg:w-64 lg:flex-shrink-0">
123
- <div className="sticky top-4">
124
- <h2 className="mb-4 text-lg font-semibold text-foreground">
125
- Filters
126
- </h2>
127
- <Suspense fallback={<Skeleton className="h-96 w-full" />}>
128
- <ProductFilters
129
- filters={filterOptions}
130
- selectedFilters={selectedFilters}
131
- onFilterChange={handleFilterChange}
132
- onClearAll={() => router.push("/products")}
133
- />
134
- </Suspense>
135
- </div>
136
- </aside>
350
+ // ========== Shared filter props ==========
351
+ const filterProps = {
352
+ filters: filterGroups,
353
+ selectedFilters,
354
+ onFilterChange: handleFilterChange,
355
+ onClearAll: handleClearAll,
356
+ };
137
357
 
138
- {/* Main Content */}
139
- <div className="flex-1">
140
- {/* Sort & Results Count */}
141
- <div className="mb-6 flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
358
+ return (
359
+ <div className="flex flex-col gap-6">
360
+ {/* Top bar: Mobile filter trigger + Sort + Result count */}
361
+ <div className="flex flex-wrap items-center justify-between gap-3">
362
+ <div className="flex items-center gap-3">
363
+ {/* Mobile filter sheet trigger */}
364
+ <FilterMobileSheet
365
+ {...filterProps}
366
+ activeFilterCount={activeFilterCount}
367
+ totalProducts={availableFilters?.totalProducts}
368
+ />
142
369
  <p className="text-sm text-muted-foreground">
143
370
  {totalCount > 0
144
- ? `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalCount)} of ${totalCount} products`
371
+ ? `${totalCount} product${totalCount !== 1 ? "s" : ""}`
145
372
  : "No products found"}
146
373
  </p>
147
- <ProductSort
148
- value={sort}
149
- onChange={(newSort) => {
150
- const newParams = new URLSearchParams(searchParams.toString());
151
- newParams.set("sort", newSort);
152
- router.push(`/products?${newParams.toString()}`);
153
- }}
154
- />
155
374
  </div>
375
+ <ProductSort value={sort} onChange={handleSortChange} />
376
+ </div>
156
377
 
157
- {/* Products Grid */}
158
- {isLoading ? (
159
- <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
160
- {Array.from({ length: 6 }).map((_, i) => (
161
- <Skeleton key={i} className="aspect-square w-full" />
162
- ))}
378
+ {/* Active filter pills */}
379
+ <FilterActivePills
380
+ pills={activePills}
381
+ onRemove={handleRemovePill}
382
+ onClearAll={handleClearAll}
383
+ />
384
+
385
+ {/* Main layout: Sidebar + Grid */}
386
+ <div className="flex gap-8">
387
+ {/* Desktop sidebar (hidden on mobile — sheet used instead) */}
388
+ <aside className="hidden w-64 flex-shrink-0 lg:block">
389
+ <div className="sticky top-4">
390
+ {isFiltersLoading && !availableFilters ? (
391
+ <div className="space-y-4">
392
+ {Array.from({ length: 3 }).map((_, i) => (
393
+ <Skeleton key={i} className="h-24 w-full" />
394
+ ))}
395
+ </div>
396
+ ) : (
397
+ <ProductFilters {...filterProps} />
398
+ )}
163
399
  </div>
164
- ) : (
165
- <ProductGrid
166
- products={products}
167
- columns={3}
168
- priorityCount={6}
169
- showBadges
170
- emptyMessage="No products match your filters"
171
- onResetFilters={() => router.push("/products")}
172
- />
173
- )}
174
-
175
- {/* Pagination */}
176
- {totalPages > 1 && (
177
- <div className="mt-8 flex justify-center">
178
- <Pagination
179
- currentPage={page}
180
- totalPages={totalPages}
181
- onPageChange={(newPage: number) => {
182
- const newParams = new URLSearchParams(searchParams.toString());
183
- newParams.set("page", newPage.toString());
184
- router.push(`/products?${newParams.toString()}`);
185
- }}
186
- />
400
+ </aside>
401
+
402
+ {/* Products grid with dimming */}
403
+ <div className="flex-1">
404
+ <div
405
+ className={cn(
406
+ "transition-opacity duration-200",
407
+ showDimming && "pointer-events-none opacity-50"
408
+ )}
409
+ data-pending={showDimming || undefined}
410
+ aria-busy={showDimming}
411
+ role="region"
412
+ aria-label="Product results"
413
+ >
414
+ {showSkeleton ? (
415
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
416
+ {Array.from({ length: 6 }).map((_, i) => (
417
+ <Skeleton key={i} className="aspect-square w-full" />
418
+ ))}
419
+ </div>
420
+ ) : (
421
+ <ProductGrid
422
+ products={products}
423
+ columns={3}
424
+ priorityCount={6}
425
+ showBadges
426
+ emptyMessage="No products match your filters"
427
+ onResetFilters={handleClearAll}
428
+ />
429
+ )}
187
430
  </div>
188
- )}
431
+
432
+ {/* Pagination */}
433
+ {totalPages > 1 && (
434
+ <div className="mt-8 flex justify-center">
435
+ <Pagination
436
+ currentPage={page}
437
+ totalPages={totalPages}
438
+ onPageChange={handlePageChange}
439
+ />
440
+ </div>
441
+ )}
442
+ </div>
189
443
  </div>
190
444
  </div>
191
445
  );
@@ -0,0 +1,18 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export default function SearchLoading() {
4
+ return (
5
+ <div className="container mx-auto px-4 py-8">
6
+ <Skeleton className="mb-8 h-12 w-full max-w-xl" />
7
+ <div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
8
+ {[...Array(8)].map((_, i) => (
9
+ <div key={i} className="space-y-3">
10
+ <Skeleton className="aspect-square w-full rounded-lg" />
11
+ <Skeleton className="h-4 w-3/4" />
12
+ <Skeleton className="h-4 w-1/2" />
13
+ </div>
14
+ ))}
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -7,6 +7,7 @@
7
7
  * and quick add to cart functionality.
8
8
  */
9
9
 
10
+ import Link from 'next/link';
10
11
  import { Heart, ShoppingCart, Trash2 } from 'lucide-react';
11
12
  import { Button } from '@/components/ui/button';
12
13
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -14,6 +15,7 @@ import { EmptyState } from '@/components/ui/empty-state';
14
15
  import { Breadcrumbs } from '@/components/layout/breadcrumbs';
15
16
  import { WishlistItem } from '@/components/wishlist/wishlist-item';
16
17
  import { useWishlistStore } from '@/stores/wishlist-store';
18
+ import { useHydrated } from '@doswiftly/storefront-sdk/react';
17
19
  import { useCartActions } from '@/hooks/use-cart-actions';
18
20
  import { toast } from 'sonner';
19
21
 
@@ -22,8 +24,8 @@ export default function WishlistPage() {
22
24
  wishlists,
23
25
  getActiveWishlist,
24
26
  clearWishlist,
25
- isHydrated,
26
27
  } = useWishlistStore();
28
+ const isHydrated = useHydrated();
27
29
 
28
30
  const { addToCart } = useCartActions();
29
31
 
@@ -113,10 +115,11 @@ export default function WishlistPage() {
113
115
  icon={<Heart className="h-12 w-12" />}
114
116
  title="Lista życzeń jest pusta"
115
117
  description="Dodaj produkty do listy życzeń, aby śledzić ich ceny i szybko dodawać do koszyka."
116
- action={{
117
- label: 'Przeglądaj produkty',
118
- href: '/products',
119
- }}
118
+ action={
119
+ <Link href="/products">
120
+ <Button>Przeglądaj produkty</Button>
121
+ </Link>
122
+ }
120
123
  />
121
124
  ) : (
122
125
  <div className="space-y-4">