@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
@@ -0,0 +1,52 @@
1
+ # Fragment for ProductDetail / ProductPage component.
2
+ #
3
+ # Extends ProductCardFields with full product data needed
4
+ # for the product detail page: gallery, variants, SEO, description.
5
+ #
6
+ # Usage in components:
7
+ # import type { ProductDetailFieldsFragment } from '@/generated/graphql';
8
+ # interface Props { product: ProductDetailFieldsFragment }
9
+ #
10
+ # Usage in custom queries:
11
+ # query ProductWithRecommendations($handle: String!) {
12
+ # product(handle: $handle) {
13
+ # ...ProductDetailFields
14
+ # }
15
+ # }
16
+
17
+ fragment ProductDetailFields on Product {
18
+ ...ProductCardFields
19
+ description
20
+ descriptionHtml
21
+ totalInventory
22
+ collectRecipientInfo
23
+ type
24
+ createdAt
25
+ updatedAt
26
+ seo {
27
+ title
28
+ description
29
+ }
30
+ images(first: 20) {
31
+ url
32
+ altText
33
+ width
34
+ height
35
+ }
36
+ variants(first: 100) {
37
+ ...ProductVariantFields
38
+ }
39
+ # Original prices (before currency conversion) for discount display
40
+ originalPriceRange {
41
+ minVariantPrice {
42
+ amount
43
+ currencyCode
44
+ }
45
+ }
46
+ originalCompareAtPriceRange {
47
+ minVariantPrice {
48
+ amount
49
+ currencyCode
50
+ }
51
+ }
52
+ }
@@ -1,15 +1,23 @@
1
1
  "use client";
2
2
 
3
- import { useState, useMemo } from "react";
4
- import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
5
- import { Input } from "@/components/ui/input";
3
+ import { useMemo } from "react";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import { Label } from "@/components/ui/label";
6
+ import {
7
+ Accordion,
8
+ AccordionItem,
9
+ AccordionTrigger,
10
+ AccordionContent,
11
+ } from "@/components/ui/accordion";
6
12
  import { Button } from "@/components/ui/button";
13
+ import { FilterPriceRange } from "./filter-price-range";
7
14
  import { cn } from "@/lib/utils";
8
15
 
9
16
  export interface FilterOption {
10
17
  label: string;
11
18
  value: string;
12
19
  count?: number;
20
+ colorHex?: string;
13
21
  }
14
22
 
15
23
  export interface FilterGroup {
@@ -19,6 +27,7 @@ export interface FilterGroup {
19
27
  options?: FilterOption[];
20
28
  min?: number;
21
29
  max?: number;
30
+ currency?: string;
22
31
  }
23
32
 
24
33
  export interface ProductFiltersProps {
@@ -30,20 +39,14 @@ export interface ProductFiltersProps {
30
39
  }
31
40
 
32
41
  /**
33
- * ProductFilters - Filter products by price, category, attributes
34
- *
35
- * @example
36
- * ```tsx
37
- * const [filters, setFilters] = useState({});
38
- *
39
- * <ProductFilters
40
- * filters={filterGroups}
41
- * selectedFilters={filters}
42
- * onFilterChange={(id, values) => {
43
- * setFilters({ ...filters, [id]: values });
44
- * }}
45
- * />
46
- * ```
42
+ * ProductFilters - Accordion-based filter panel
43
+ *
44
+ * Supports:
45
+ * - Checkbox groups (categories, attributes) with Radix Checkbox
46
+ * - Color swatches with visual selection
47
+ * - Price range with dual-thumb Radix Slider
48
+ * - Filter count badges in accordion triggers
49
+ * - Clear all button
47
50
  */
48
51
  export function ProductFilters({
49
52
  filters,
@@ -52,11 +55,6 @@ export function ProductFilters({
52
55
  onClearAll,
53
56
  className,
54
57
  }: ProductFiltersProps) {
55
- const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({
56
- min: "",
57
- max: "",
58
- });
59
-
60
58
  const handleCheckboxChange = (filterId: string, value: string) => {
61
59
  const current = selectedFilters[filterId] || [];
62
60
  const updated = current.includes(value)
@@ -65,20 +63,31 @@ export function ProductFilters({
65
63
  onFilterChange(filterId, updated);
66
64
  };
67
65
 
68
- const handlePriceRangeApply = (filterId: string, min: number, max: number) => {
69
- onFilterChange(filterId, [`${min}-${max}`]);
66
+ const handlePriceRangeChange = (
67
+ filterId: string,
68
+ min: number | undefined,
69
+ max: number | undefined
70
+ ) => {
71
+ if (min === undefined && max === undefined) {
72
+ onFilterChange(filterId, []);
73
+ } else {
74
+ onFilterChange(filterId, [`${min ?? 0}-${max ?? 999999}`]);
75
+ }
70
76
  };
71
77
 
72
78
  const hasActiveFilters = Object.values(selectedFilters).some(
73
79
  (values) => values.length > 0
74
80
  );
75
81
 
76
- // Memoize default open filter IDs to avoid re-creating array on every render
77
- const defaultOpenFilters = useMemo(() => filters.map((f) => f.id), [filters]);
82
+ const defaultOpenFilters = useMemo(
83
+ () => filters.slice(0, 5).map((f) => f.id),
84
+ [filters]
85
+ );
86
+
87
+ if (filters.length === 0) return null;
78
88
 
79
89
  return (
80
90
  <div className={cn("space-y-4", className)}>
81
- {/* Clear all button */}
82
91
  {hasActiveFilters && onClearAll && (
83
92
  <Button
84
93
  variant="outline"
@@ -90,7 +99,6 @@ export function ProductFilters({
90
99
  </Button>
91
100
  )}
92
101
 
93
- {/* Filter groups */}
94
102
  <Accordion type="multiple" defaultValue={defaultOpenFilters}>
95
103
  {filters.map((filter) => (
96
104
  <AccordionItem key={filter.id} value={filter.id}>
@@ -107,106 +115,44 @@ export function ProductFilters({
107
115
  <AccordionContent value={filter.id}>
108
116
  {/* Checkbox filters */}
109
117
  {filter.type === "checkbox" && filter.options && (
110
- <div className="space-y-2">
111
- {filter.options.map((option) => (
112
- <label
113
- key={option.value}
114
- className="flex items-center gap-2 cursor-pointer"
115
- >
116
- <input
117
- type="checkbox"
118
- checked={selectedFilters[filter.id]?.includes(
119
- option.value
120
- )}
121
- onChange={() =>
122
- handleCheckboxChange(filter.id, option.value)
123
- }
124
- className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring focus:ring-offset-2"
125
- />
126
- <span className="flex-1 text-sm text-foreground">
127
- {option.label}
128
- </span>
129
- {option.count !== undefined && (
130
- <span className="text-xs text-muted-foreground">
131
- ({option.count})
132
- </span>
133
- )}
134
- </label>
135
- ))}
136
- </div>
118
+ <CheckboxFilterGroup
119
+ filterId={filter.id}
120
+ options={filter.options}
121
+ selected={selectedFilters[filter.id] || []}
122
+ onToggle={handleCheckboxChange}
123
+ />
137
124
  )}
138
125
 
139
- {/* Price range filter */}
140
- {filter.type === "range" && (
141
- <div className="space-y-3">
142
- <div className="flex items-center gap-2">
143
- <Input
144
- type="number"
145
- placeholder="Min"
146
- value={priceRange.min}
147
- onChange={(e) =>
148
- setPriceRange({ ...priceRange, min: e.target.value })
149
- }
150
- min={filter.min}
151
- max={filter.max}
152
- className="w-full"
153
- />
154
- <span className="text-muted-foreground">–</span>
155
- <Input
156
- type="number"
157
- placeholder="Max"
158
- value={priceRange.max}
159
- onChange={(e) =>
160
- setPriceRange({ ...priceRange, max: e.target.value })
161
- }
162
- min={filter.min}
163
- max={filter.max}
164
- className="w-full"
165
- />
166
- </div>
167
- <Button
168
- size="sm"
169
- onClick={() =>
170
- handlePriceRangeApply(
171
- filter.id,
172
- parseFloat(priceRange.min) || filter.min || 0,
173
- parseFloat(priceRange.max) || filter.max || 999999
174
- )
126
+ {/* Price / numeric range filter */}
127
+ {filter.type === "range" &&
128
+ filter.min != null &&
129
+ filter.max != null && (
130
+ <FilterPriceRange
131
+ min={filter.min}
132
+ max={filter.max}
133
+ currentMin={parseRangeValue(
134
+ selectedFilters[filter.id],
135
+ "min"
136
+ )}
137
+ currentMax={parseRangeValue(
138
+ selectedFilters[filter.id],
139
+ "max"
140
+ )}
141
+ currency={filter.currency}
142
+ onChange={(min, max) =>
143
+ handlePriceRangeChange(filter.id, min, max)
175
144
  }
176
- className="w-full"
177
- >
178
- Apply
179
- </Button>
180
- </div>
181
- )}
145
+ />
146
+ )}
182
147
 
183
- {/* Color filters */}
148
+ {/* Color swatch filters */}
184
149
  {filter.type === "color" && filter.options && (
185
- <div className="flex flex-wrap gap-2">
186
- {filter.options.map((option) => {
187
- const isSelected = selectedFilters[filter.id]?.includes(
188
- option.value
189
- );
190
- return (
191
- <button
192
- key={option.value}
193
- type="button"
194
- onClick={() =>
195
- handleCheckboxChange(filter.id, option.value)
196
- }
197
- className={cn(
198
- "h-8 w-8 rounded-full border-2 transition-all",
199
- isSelected
200
- ? "border-primary ring-2 ring-ring ring-offset-2"
201
- : "border-border hover:border-primary"
202
- )}
203
- style={{ backgroundColor: option.value }}
204
- title={option.label}
205
- aria-label={option.label}
206
- />
207
- );
208
- })}
209
- </div>
150
+ <ColorSwatchGroup
151
+ filterId={filter.id}
152
+ options={filter.options}
153
+ selected={selectedFilters[filter.id] || []}
154
+ onToggle={handleCheckboxChange}
155
+ />
210
156
  )}
211
157
  </AccordionContent>
212
158
  </AccordionItem>
@@ -215,3 +161,110 @@ export function ProductFilters({
215
161
  </div>
216
162
  );
217
163
  }
164
+
165
+ // ============================================================================
166
+ // SUB-COMPONENTS
167
+ // ============================================================================
168
+
169
+ function CheckboxFilterGroup({
170
+ filterId,
171
+ options,
172
+ selected,
173
+ onToggle,
174
+ }: {
175
+ filterId: string;
176
+ options: FilterOption[];
177
+ selected: string[];
178
+ onToggle: (filterId: string, value: string) => void;
179
+ }) {
180
+ return (
181
+ <div className="space-y-2">
182
+ {options.map((option) => {
183
+ const isChecked = selected.includes(option.value);
184
+ const checkboxId = `${filterId}-${option.value}`;
185
+
186
+ return (
187
+ <div key={option.value} className="flex items-center gap-2">
188
+ <Checkbox
189
+ id={checkboxId}
190
+ checked={isChecked}
191
+ onCheckedChange={() => onToggle(filterId, option.value)}
192
+ />
193
+ <Label
194
+ htmlFor={checkboxId}
195
+ className="flex flex-1 cursor-pointer items-center justify-between text-sm"
196
+ >
197
+ <span>{option.label}</span>
198
+ {option.count !== undefined && (
199
+ <span className="text-xs text-muted-foreground">
200
+ ({option.count})
201
+ </span>
202
+ )}
203
+ </Label>
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ );
209
+ }
210
+
211
+ function ColorSwatchGroup({
212
+ filterId,
213
+ options,
214
+ selected,
215
+ onToggle,
216
+ }: {
217
+ filterId: string;
218
+ options: FilterOption[];
219
+ selected: string[];
220
+ onToggle: (filterId: string, value: string) => void;
221
+ }) {
222
+ return (
223
+ <div className="flex flex-wrap gap-3">
224
+ {options.map((option) => {
225
+ const isSelected = selected.includes(option.value);
226
+ const bgColor = option.colorHex || option.value;
227
+
228
+ return (
229
+ <button
230
+ key={option.value}
231
+ type="button"
232
+ onClick={() => onToggle(filterId, option.value)}
233
+ className="group flex flex-col items-center gap-1"
234
+ title={option.label}
235
+ aria-label={`${option.label}${option.count !== undefined ? ` (${option.count})` : ""}`}
236
+ aria-pressed={isSelected}
237
+ >
238
+ <span
239
+ className={cn(
240
+ "block h-8 w-8 rounded-full border-2 transition-all",
241
+ isSelected
242
+ ? "border-primary ring-2 ring-ring ring-offset-2"
243
+ : "border-border group-hover:border-primary"
244
+ )}
245
+ style={{ backgroundColor: bgColor }}
246
+ />
247
+ <span className="text-[10px] text-muted-foreground">
248
+ {option.label}
249
+ {option.count !== undefined && ` (${option.count})`}
250
+ </span>
251
+ </button>
252
+ );
253
+ })}
254
+ </div>
255
+ );
256
+ }
257
+
258
+ // ============================================================================
259
+ // HELPERS
260
+ // ============================================================================
261
+
262
+ function parseRangeValue(
263
+ values: string[] | undefined,
264
+ part: "min" | "max"
265
+ ): number | undefined {
266
+ if (!values || values.length === 0) return undefined;
267
+ const [minStr, maxStr] = values[0].split("-");
268
+ const val = parseFloat(part === "min" ? minStr : maxStr);
269
+ return isNaN(val) ? undefined : val;
270
+ }
@@ -1,14 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { ProductCard, type ProductCardProduct } from "./product-card";
3
+ import { ProductCard } from "./product-card";
4
4
  import { EmptyProducts } from "@/components/ui/empty-state";
5
5
  import { cn } from "@/lib/utils";
6
-
7
- // Re-export for external use
8
- export type { ProductCardProduct };
6
+ import type { ProductCardFields } from "@/lib/graphql/fragments";
9
7
 
10
8
  export interface ProductGridProps {
11
- products: ProductCardProduct[];
9
+ products: ProductCardFields[];
12
10
  className?: string;
13
11
  columns?: 2 | 3 | 4 | 5;
14
12
  gap?: "sm" | "md" | "lg";
@@ -7,8 +7,8 @@ import { cn } from "@/lib/utils";
7
7
  export interface ProductImageData {
8
8
  url: string;
9
9
  altText?: string | null;
10
- width?: number;
11
- height?: number;
10
+ width?: number | null;
11
+ height?: number | null;
12
12
  }
13
13
 
14
14
  export interface ProductImageProps {
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
- import { formatPrice } from "@/lib/format";
5
- import type { PriceMoney } from "@/lib/format";
4
+ import { formatPrice } from "@doswiftly/storefront-sdk";
5
+ import type { PriceMoney } from "@doswiftly/storefront-sdk";
6
6
 
7
7
  export interface ProductPriceMoney {
8
8
  amount: string;
@@ -199,10 +199,11 @@ export function ProductReviews({
199
199
  icon={<MessageSquare className="h-12 w-12" />}
200
200
  title="Brak opinii"
201
201
  description="Bądź pierwszą osobą, która podzieli się opinią o tym produkcie."
202
- action={onSubmitReview ? {
203
- label: 'Napisz opinię',
204
- onClick: () => setShowForm(true),
205
- } : undefined}
202
+ action={onSubmitReview ? (
203
+ <Button onClick={() => setShowForm(true)}>
204
+ Napisz opinię
205
+ </Button>
206
+ ) : undefined}
206
207
  />
207
208
  ) : (
208
209
  <div className="space-y-4">
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { Select } from "@/components/ui/select";
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@/components/ui/select";
4
10
 
5
11
  // Sort options that match backend expectations
6
12
  // Backend should normalize these to ProductSortKeys + reverse flag
@@ -47,12 +53,18 @@ const sortOptions: { value: SortOption; label: string }[] = [
47
53
  export function ProductSort({ value, onChange, className }: ProductSortProps) {
48
54
  return (
49
55
  <div className={className}>
50
- <Select
51
- value={value}
52
- onChange={(e) => onChange(e.target.value as SortOption)}
53
- label="Sort by"
54
- options={sortOptions}
55
- />
56
+ <Select value={value} onValueChange={(val) => onChange(val as SortOption)}>
57
+ <SelectTrigger>
58
+ <SelectValue placeholder="Sort by" />
59
+ </SelectTrigger>
60
+ <SelectContent>
61
+ {sortOptions.map((option) => (
62
+ <SelectItem key={option.value} value={option.value}>
63
+ {option.label}
64
+ </SelectItem>
65
+ ))}
66
+ </SelectContent>
67
+ </Select>
56
68
  </div>
57
69
  );
58
70
  }
@@ -2,36 +2,21 @@
2
2
 
3
3
  import { useState, useEffect } from "react";
4
4
  import { cn } from "@/lib/utils";
5
-
6
- export interface VariantOption {
7
- name: string;
8
- value: string;
9
- }
10
-
11
- export interface ProductVariant {
12
- id: string;
13
- title: string;
14
- available: boolean;
15
- selectedOptions: VariantOption[];
16
- price: {
17
- amount: string;
18
- currencyCode: string;
19
- };
20
- }
5
+ import type { ProductVariantFields } from "@/lib/graphql/fragments";
21
6
 
22
7
  export interface ProductVariantSelectorProps {
23
- variants: ProductVariant[];
8
+ variants: ProductVariantFields[];
24
9
  selectedVariantId?: string;
25
- onVariantChange: (variant: ProductVariant) => void;
10
+ onVariantChange: (variant: ProductVariantFields) => void;
26
11
  className?: string;
27
12
  }
28
13
 
29
14
  /**
30
15
  * ProductVariantSelector - Select product options (size, color, etc.)
31
- *
16
+ *
32
17
  * Extracts unique options from variants and allows selection.
33
18
  * Automatically finds matching variant when options change.
34
- *
19
+ *
35
20
  * @example
36
21
  * ```tsx
37
22
  * <ProductVariantSelector
@@ -78,12 +63,12 @@ export function ProductVariantSelector({
78
63
  ...acc,
79
64
  [opt.name]: opt.value,
80
65
  }),
81
- {}
66
+ {} as Record<string, string>
82
67
  );
83
68
  });
84
69
 
85
70
  // Find variant matching current selection
86
- const findMatchingVariant = (opts: Record<string, string>): ProductVariant | undefined => {
71
+ const findMatchingVariant = (opts: Record<string, string>): ProductVariantFields | undefined => {
87
72
  return variants.find((variant) =>
88
73
  variant.selectedOptions.every((opt) => opts[opt.name] === opt.value)
89
74
  );
@@ -114,7 +99,7 @@ export function ProductVariantSelector({
114
99
  if (variant) {
115
100
  const opts = variant.selectedOptions.reduce(
116
101
  (acc, opt) => ({ ...acc, [opt.name]: opt.value }),
117
- {}
102
+ {} as Record<string, string>
118
103
  );
119
104
  setSelectedOptions(opts);
120
105
  }
@@ -0,0 +1,51 @@
1
+ # Fragment for ProductVariant data.
2
+ #
3
+ # Used within ProductDetailFields and any component that renders
4
+ # variant selectors (size, color), pricing, and availability.
5
+ #
6
+ # Usage in components:
7
+ # import type { ProductVariantFieldsFragment } from '@/generated/graphql';
8
+ # interface Props { variant: ProductVariantFieldsFragment }
9
+
10
+ fragment ProductVariantFields on ProductVariant {
11
+ id
12
+ title
13
+ sku
14
+ available
15
+ quantityAvailable
16
+ barcode
17
+ weight
18
+ position
19
+ price {
20
+ amount
21
+ currencyCode
22
+ baseAmount
23
+ baseCurrencyCode
24
+ isConverted
25
+ }
26
+ originalPrice {
27
+ amount
28
+ currencyCode
29
+ }
30
+ compareAtPrice {
31
+ amount
32
+ currencyCode
33
+ baseAmount
34
+ baseCurrencyCode
35
+ isConverted
36
+ }
37
+ originalCompareAtPrice {
38
+ amount
39
+ currencyCode
40
+ }
41
+ image {
42
+ url
43
+ altText
44
+ width
45
+ height
46
+ }
47
+ selectedOptions {
48
+ name
49
+ value
50
+ }
51
+ }
@@ -110,7 +110,7 @@ export function ReviewCard({ review, onVoteHelpful, className }: ReviewCardProps
110
110
  <p className="text-foreground whitespace-pre-line">{displayContent}</p>
111
111
  {shouldTruncate && (
112
112
  <Button
113
- variant="link"
113
+ variant="ghost"
114
114
  size="sm"
115
115
  className="p-0 h-auto mt-1"
116
116
  onClick={() => setExpanded(!expanded)}
@@ -16,12 +16,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
16
16
  import { cn } from '@/lib/utils';
17
17
  import { toast } from 'sonner';
18
18
 
19
- // Ensure Textarea exists or create a simple version
20
- const TextareaComponent = typeof Textarea === 'undefined'
21
- ? ({ className, ...props }: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => (
22
- <textarea className={cn('flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', className)} {...props} />
23
- )
24
- : Textarea;
25
19
 
26
20
  interface ReviewFormProps {
27
21
  productId: string;
@@ -196,7 +190,7 @@ export function ReviewForm({
196
190
  {/* Content */}
197
191
  <div>
198
192
  <Label htmlFor="content">Treść opinii *</Label>
199
- <TextareaComponent
193
+ <Textarea
200
194
  id="content"
201
195
  value={content}
202
196
  onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setContent(e.target.value)}