@doswiftly/cli 0.1.17 → 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.
- package/README.md +23 -323
- package/dist/commands/check.js +1 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +43 -20
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +4 -4
- package/dist/commands/sdk.js +5 -5
- package/dist/commands/sdk.js.map +1 -1
- package/dist/commands/template.js +4 -4
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/types.js +5 -5
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/verify.js +2 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/lib/package-manager.d.ts +1 -1
- package/dist/lib/package-manager.js +1 -1
- package/package.json +1 -1
- package/templates/storefront-minimal/wrangler.toml +4 -0
- package/templates/storefront-nextjs/README.md +16 -12
- package/templates/storefront-nextjs/app/account/orders/page.tsx +2 -2
- package/templates/storefront-nextjs/app/account/page.tsx +2 -2
- package/templates/storefront-nextjs/app/auth/login/page.tsx +1 -1
- package/templates/storefront-nextjs/app/auth/register/page.tsx +1 -1
- package/templates/storefront-nextjs/app/cart/page.tsx +1 -1
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-nextjs/app/categories/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/page.tsx +1 -1
- package/templates/storefront-nextjs/app/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/page.tsx +2 -2
- package/templates/storefront-nextjs/app/search/page.tsx +1 -1
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +1 -1
- package/templates/storefront-nextjs/components/providers.tsx +1 -1
- package/templates/storefront-nextjs/lib/currency.tsx +3 -3
- package/templates/storefront-nextjs/lib/format.ts +1 -1
- package/templates/storefront-nextjs/lib/graphql-queries.ts +3 -3
- package/templates/storefront-nextjs/package.dev.json +1 -1
- package/templates/storefront-nextjs/package.json +1 -1
- package/templates/storefront-nextjs/package.json.template +1 -1
- package/templates/storefront-nextjs/wrangler.toml +4 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +47 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +47 -0
- package/templates/storefront-nextjs-shadcn/CLAUDE.md +148 -35
- package/templates/storefront-nextjs-shadcn/README.md +29 -162
- package/templates/storefront-nextjs-shadcn/app/account/addresses/page.tsx +98 -91
- package/templates/storefront-nextjs-shadcn/app/account/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/account/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/account/loyalty/page.tsx +53 -162
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/loading.tsx +60 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/page.tsx +36 -47
- package/templates/storefront-nextjs-shadcn/app/account/orders/page.tsx +46 -29
- package/templates/storefront-nextjs-shadcn/app/account/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/app/account/settings/page.tsx +108 -71
- package/templates/storefront-nextjs-shadcn/app/api/auth/clear-token/route.ts +2 -86
- package/templates/storefront-nextjs-shadcn/app/api/auth/set-token/route.ts +2 -124
- package/templates/storefront-nextjs-shadcn/app/auth/forgot-password/page.tsx +10 -5
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/loading.tsx +17 -0
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/page.tsx +43 -2
- package/templates/storefront-nextjs-shadcn/app/blog/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/brands/page.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/app/cart/loading.tsx +26 -0
- package/templates/storefront-nextjs-shadcn/app/cart/page.tsx +6 -3
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/category-products-client.tsx +56 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/page.tsx +76 -59
- package/templates/storefront-nextjs-shadcn/app/categories/page.tsx +8 -4
- package/templates/storefront-nextjs-shadcn/app/checkout/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/loading.tsx +31 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/page.tsx +116 -79
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/page.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/app/collections/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/collections/page.tsx +7 -4
- package/templates/storefront-nextjs-shadcn/app/global-error.tsx +117 -0
- package/templates/storefront-nextjs-shadcn/app/globals.css +8 -0
- package/templates/storefront-nextjs-shadcn/app/layout.tsx +46 -11
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/loading.tsx +29 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/page.tsx +6 -6
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/product-client.tsx +15 -61
- package/templates/storefront-nextjs-shadcn/app/products/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/products/products-client.tsx +405 -151
- package/templates/storefront-nextjs-shadcn/app/search/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/wishlist/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/codegen.ts +48 -31
- package/templates/storefront-nextjs-shadcn/components/account/customer-info.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +26 -24
- package/templates/storefront-nextjs-shadcn/components/account/order-summary.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +9 -9
- package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +11 -37
- package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +37 -23
- package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +4 -3
- package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/cart-line.fragment.graphql +53 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +22 -7
- package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/common/price-display.tsx +35 -11
- package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +3 -3
- package/templates/storefront-nextjs-shadcn/components/filters/range-slider-filter.tsx +5 -5
- package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/components/home/collection-card.fragment.graphql +21 -0
- package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +2 -12
- package/templates/storefront-nextjs-shadcn/components/home/index.ts +0 -1
- package/templates/storefront-nextjs-shadcn/components/hydrated.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +4 -4
- package/templates/storefront-nextjs-shadcn/components/layout/category-node.fragment.graphql +22 -0
- package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +33 -23
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-balance.tsx +2 -11
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-history.tsx +8 -25
- package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +10 -19
- package/templates/storefront-nextjs-shadcn/components/loyalty/rewards-catalog.tsx +17 -41
- package/templates/storefront-nextjs-shadcn/components/loyalty/tier-progress.tsx +2 -29
- package/templates/storefront-nextjs-shadcn/components/order/index.ts +6 -1
- package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/product/filter-active-pills.tsx +69 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-mobile-sheet.tsx +84 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-price-range.tsx +138 -0
- package/templates/storefront-nextjs-shadcn/components/product/index.ts +9 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-card.fragment.graphql +49 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +3 -31
- package/templates/storefront-nextjs-shadcn/components/product/product-detail.fragment.graphql +52 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +176 -123
- package/templates/storefront-nextjs-shadcn/components/product/product-grid.tsx +3 -5
- package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-price.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-reviews.tsx +5 -4
- package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +19 -7
- package/templates/storefront-nextjs-shadcn/components/product/product-variant-selector.tsx +8 -23
- package/templates/storefront-nextjs-shadcn/components/product/product-variant.fragment.graphql +51 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-card.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +1 -7
- package/templates/storefront-nextjs-shadcn/components/product/savings-display.tsx +17 -2
- package/templates/storefront-nextjs-shadcn/components/product/similar-products.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/providers/index.ts +1 -1
- package/templates/storefront-nextjs-shadcn/components/providers/stores-provider.tsx +30 -0
- package/templates/storefront-nextjs-shadcn/components/providers/theme-provider.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/returns/index.ts +2 -2
- package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/search/search-results.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/ui/form.tsx +174 -0
- package/templates/storefront-nextjs-shadcn/components/ui/index.ts +30 -2
- package/templates/storefront-nextjs-shadcn/components/ui/progress.tsx +40 -0
- package/templates/storefront-nextjs-shadcn/components/ui/sheet.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/components/ui/slider.tsx +33 -0
- package/templates/storefront-nextjs-shadcn/components/ui/textarea.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/generated/graphql.ts +12779 -0
- package/templates/storefront-nextjs-shadcn/graphql/custom.example.graphql +159 -0
- package/templates/storefront-nextjs-shadcn/hooks/index.ts +2 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth-sync.ts +42 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth.ts +17 -295
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +51 -19
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +13 -9
- package/templates/storefront-nextjs-shadcn/lib/auth/routes.ts +4 -17
- package/templates/storefront-nextjs-shadcn/lib/graphql/client.ts +22 -99
- package/templates/storefront-nextjs-shadcn/lib/graphql/config.ts +32 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/fragments.ts +34 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +687 -632
- package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +86 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +131 -182
- package/templates/storefront-nextjs-shadcn/lib/graphql/types.ts +62 -0
- package/templates/storefront-nextjs-shadcn/lib/theme/theme-config.ts +0 -17
- package/templates/storefront-nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/storefront-nextjs-shadcn/package.dev.json +1 -3
- package/templates/storefront-nextjs-shadcn/package.json +12 -13
- package/templates/storefront-nextjs-shadcn/package.json.template +6 -7
- package/templates/storefront-nextjs-shadcn/proxy.ts +3 -4
- package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +41 -39
- package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +64 -75
- package/templates/storefront-nextjs-shadcn/stores/wishlist-store.ts +178 -177
- package/templates/storefront-nextjs-shadcn/tsconfig.json +23 -5
- package/templates/storefront-nextjs-shadcn/wrangler.toml +4 -0
- package/templates/storefront-nextjs-shadcn/CART_INTEGRATION.md +0 -282
- package/templates/storefront-nextjs-shadcn/GRAPHQL_DOCUMENT_NAMES.md +0 -190
- package/templates/storefront-nextjs-shadcn/GRAPHQL_ERROR_HANDLING.md +0 -263
- package/templates/storefront-nextjs-shadcn/GRAPHQL_FIXES_SUMMARY.md +0 -135
- package/templates/storefront-nextjs-shadcn/GRAPHQL_INTEGRATION_COMPLETE.md +0 -142
- package/templates/storefront-nextjs-shadcn/INTEGRATION_CHECKLIST.md +0 -448
- package/templates/storefront-nextjs-shadcn/PRODUCT_DETAIL_PAGE_IMPLEMENTATION.md +0 -307
- package/templates/storefront-nextjs-shadcn/THEME_CUSTOMIZATION.md +0 -245
- package/templates/storefront-nextjs-shadcn/components/providers/currency-provider.tsx +0 -103
- package/templates/storefront-nextjs-shadcn/graphql/collections.example.ts +0 -168
- package/templates/storefront-nextjs-shadcn/graphql/products.example.ts +0 -160
- package/templates/storefront-nextjs-shadcn/lib/auth/cookies.ts +0 -220
- package/templates/storefront-nextjs-shadcn/lib/config.ts +0 -46
- package/templates/storefront-nextjs-shadcn/lib/currency/IMPLEMENTATION_SUMMARY.md +0 -254
- package/templates/storefront-nextjs-shadcn/lib/currency/README.md +0 -464
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.test.ts +0 -328
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.ts +0 -295
- package/templates/storefront-nextjs-shadcn/lib/currency/index.ts +0 -27
- package/templates/storefront-nextjs-shadcn/lib/format.ts +0 -226
- package/templates/storefront-nextjs-shadcn/lib/hooks.ts +0 -30
- package/templates/storefront-nextjs-shadcn/stores/auth-store.ts +0 -66
- 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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 -
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
<
|
|
111
|
-
{filter.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
186
|
-
{filter.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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:
|
|
9
|
+
products: ProductCardFields[];
|
|
12
10
|
className?: string;
|
|
13
11
|
columns?: 2 | 3 | 4 | 5;
|
|
14
12
|
gap?: "sm" | "md" | "lg";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
|
-
import { formatPrice } from "
|
|
5
|
-
import type { PriceMoney } from "
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 {
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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:
|
|
8
|
+
variants: ProductVariantFields[];
|
|
24
9
|
selectedVariantId?: string;
|
|
25
|
-
onVariantChange: (variant:
|
|
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>):
|
|
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
|
}
|
package/templates/storefront-nextjs-shadcn/components/product/product-variant.fragment.graphql
ADDED
|
@@ -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="
|
|
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
|
-
<
|
|
193
|
+
<Textarea
|
|
200
194
|
id="content"
|
|
201
195
|
value={content}
|
|
202
196
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setContent(e.target.value)}
|