@doswiftly/cli 0.1.23 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/check.js +2 -2
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +34 -7
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +155 -63
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +271 -166
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/sdk.d.ts +1 -1
- package/dist/commands/sdk.js +3 -3
- package/dist/commands/sdk.js.map +1 -1
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/commands/template.js +4 -31
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/verify.js +5 -5
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/i18n.d.ts +12 -0
- package/dist/lib/i18n.d.ts.map +1 -1
- package/dist/lib/i18n.js +24 -0
- package/dist/lib/i18n.js.map +1 -1
- package/dist/lib/proxy-server.d.ts +22 -6
- package/dist/lib/proxy-server.d.ts.map +1 -1
- package/dist/lib/proxy-server.js +174 -75
- package/dist/lib/proxy-server.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/types.d.ts +0 -5
- package/dist/commands/types.d.ts.map +0 -1
- package/dist/commands/types.js +0 -82
- package/dist/commands/types.js.map +0 -1
- package/templates/storefront-minimal/.env.example +0 -10
- package/templates/storefront-minimal/.github/workflows/build-template.yml +0 -119
- package/templates/storefront-minimal/app/globals.css +0 -18
- package/templates/storefront-minimal/app/layout.tsx +0 -26
- package/templates/storefront-minimal/app/page.tsx +0 -93
- package/templates/storefront-minimal/lib/graphql-client.ts +0 -23
- package/templates/storefront-minimal/next.config.ts +0 -15
- package/templates/storefront-minimal/open-next.config.ts +0 -3
- package/templates/storefront-minimal/package.json +0 -30
- package/templates/storefront-minimal/postcss.config.mjs +0 -5
- package/templates/storefront-minimal/tailwind.config.ts +0 -14
- package/templates/storefront-minimal/tsconfig.json +0 -27
- package/templates/storefront-minimal/wrangler.toml +0 -24
- package/templates/storefront-nextjs/.env.example +0 -68
- package/templates/storefront-nextjs/.github/workflows/build-template.yml +0 -119
- package/templates/storefront-nextjs/README.md +0 -524
- package/templates/storefront-nextjs/app/account/orders/page.tsx +0 -216
- package/templates/storefront-nextjs/app/account/page.tsx +0 -167
- package/templates/storefront-nextjs/app/auth/login/page.tsx +0 -135
- package/templates/storefront-nextjs/app/auth/register/page.tsx +0 -212
- package/templates/storefront-nextjs/app/cart/page.tsx +0 -263
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +0 -200
- package/templates/storefront-nextjs/app/categories/page.tsx +0 -58
- package/templates/storefront-nextjs/app/checkout/page.tsx +0 -351
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +0 -158
- package/templates/storefront-nextjs/app/collections/page.tsx +0 -61
- package/templates/storefront-nextjs/app/globals.css +0 -98
- package/templates/storefront-nextjs/app/layout.tsx +0 -39
- package/templates/storefront-nextjs/app/page.tsx +0 -136
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +0 -119
- package/templates/storefront-nextjs/app/products/page.tsx +0 -107
- package/templates/storefront-nextjs/app/search/page.tsx +0 -127
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +0 -94
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +0 -77
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +0 -29
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +0 -217
- package/templates/storefront-nextjs/components/commerce/pagination.tsx +0 -62
- package/templates/storefront-nextjs/components/commerce/product-actions.tsx +0 -135
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +0 -109
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +0 -375
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +0 -178
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +0 -64
- package/templates/storefront-nextjs/components/commerce/variant-selector.tsx +0 -210
- package/templates/storefront-nextjs/components/layout/footer.tsx +0 -107
- package/templates/storefront-nextjs/components/layout/header.tsx +0 -104
- package/templates/storefront-nextjs/components/providers.tsx +0 -62
- package/templates/storefront-nextjs/lib/auth/routes.ts +0 -52
- package/templates/storefront-nextjs/lib/currency.tsx +0 -140
- package/templates/storefront-nextjs/lib/format.ts +0 -159
- package/templates/storefront-nextjs/lib/graphql-queries.ts +0 -629
- package/templates/storefront-nextjs/lib/hooks.ts +0 -30
- package/templates/storefront-nextjs/middleware.ts +0 -80
- package/templates/storefront-nextjs/next.config.ts +0 -37
- package/templates/storefront-nextjs/open-next.config.ts +0 -3
- package/templates/storefront-nextjs/package.dev.json +0 -30
- package/templates/storefront-nextjs/package.json +0 -32
- package/templates/storefront-nextjs/package.json.template +0 -32
- package/templates/storefront-nextjs/postcss.config.mjs +0 -8
- package/templates/storefront-nextjs/tailwind.config.ts +0 -111
- package/templates/storefront-nextjs/tsconfig.json +0 -27
- package/templates/storefront-nextjs/wrangler.toml +0 -24
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
4
|
-
import { useCallback } from "react";
|
|
5
|
-
import { ProductSortKeys } from "@doswiftly/storefront-sdk/graphql";
|
|
6
|
-
|
|
7
|
-
interface Category {
|
|
8
|
-
id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
slug: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ProductFiltersProps {
|
|
14
|
-
categories: Category[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* ProductFilters - URL-based filtering for products
|
|
19
|
-
*
|
|
20
|
-
* Uses Next.js router to update URL search params.
|
|
21
|
-
* Server Component re-fetches data automatically.
|
|
22
|
-
*/
|
|
23
|
-
export function ProductFilters({ categories }: ProductFiltersProps) {
|
|
24
|
-
const router = useRouter();
|
|
25
|
-
const pathname = usePathname();
|
|
26
|
-
const searchParams = useSearchParams();
|
|
27
|
-
|
|
28
|
-
// Get current filter values
|
|
29
|
-
const currentCategory = searchParams.get("category") || "";
|
|
30
|
-
const currentSort = searchParams.get("sort") || ProductSortKeys.CreatedAt;
|
|
31
|
-
const currentOrder = searchParams.get("order") || "desc";
|
|
32
|
-
|
|
33
|
-
// Update URL with new params
|
|
34
|
-
const updateParams = useCallback(
|
|
35
|
-
(updates: Record<string, string | null>) => {
|
|
36
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
37
|
-
|
|
38
|
-
Object.entries(updates).forEach(([key, value]) => {
|
|
39
|
-
if (value === null || value === "") {
|
|
40
|
-
params.delete(key);
|
|
41
|
-
} else {
|
|
42
|
-
params.set(key, value);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Reset pagination when filters change
|
|
47
|
-
params.delete("after");
|
|
48
|
-
|
|
49
|
-
router.push(`${pathname}?${params.toString()}`);
|
|
50
|
-
},
|
|
51
|
-
[router, pathname, searchParams]
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
55
|
-
updateParams({ category: e.target.value || null });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
59
|
-
const value = e.target.value;
|
|
60
|
-
// Parse combined sort value (e.g., "PRICE_asc" or "CREATED_AT_desc")
|
|
61
|
-
const [sortKey, order] = value.includes("_")
|
|
62
|
-
? value.split("_")
|
|
63
|
-
: [value, "desc"];
|
|
64
|
-
updateParams({ sort: sortKey, order });
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Combined sort value for select
|
|
68
|
-
const sortValue =
|
|
69
|
-
currentSort === ProductSortKeys.Price && currentOrder === "asc"
|
|
70
|
-
? "PRICE_asc"
|
|
71
|
-
: currentSort === ProductSortKeys.Price && currentOrder === "desc"
|
|
72
|
-
? "PRICE_desc"
|
|
73
|
-
: currentSort;
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div className="mb-8 flex flex-wrap items-center justify-between gap-4">
|
|
77
|
-
{/* Category Filter */}
|
|
78
|
-
<div className="flex gap-2">
|
|
79
|
-
<select
|
|
80
|
-
className="input w-auto"
|
|
81
|
-
value={currentCategory}
|
|
82
|
-
onChange={handleCategoryChange}
|
|
83
|
-
>
|
|
84
|
-
<option value="">All Categories</option>
|
|
85
|
-
{categories.map((cat) => (
|
|
86
|
-
<option key={cat.id} value={cat.slug}>
|
|
87
|
-
{cat.name}
|
|
88
|
-
</option>
|
|
89
|
-
))}
|
|
90
|
-
</select>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
{/* Sort */}
|
|
94
|
-
<div className="flex gap-2">
|
|
95
|
-
<select
|
|
96
|
-
className="input w-auto"
|
|
97
|
-
value={sortValue}
|
|
98
|
-
onChange={handleSortChange}
|
|
99
|
-
>
|
|
100
|
-
<option value={ProductSortKeys.CreatedAt}>Newest</option>
|
|
101
|
-
<option value="PRICE_asc">Price: Low to High</option>
|
|
102
|
-
<option value="PRICE_desc">Price: High to Low</option>
|
|
103
|
-
<option value={ProductSortKeys.Title}>Name: A-Z</option>
|
|
104
|
-
<option value={ProductSortKeys.BestSelling}>Most Popular</option>
|
|
105
|
-
</select>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo } from "react";
|
|
4
|
-
import { formatPrice, formatAmount } from "@/lib/currency";
|
|
5
|
-
import type {
|
|
6
|
-
PriceMoney,
|
|
7
|
-
Money,
|
|
8
|
-
ConvertedPriceRange,
|
|
9
|
-
ProductPriceRange,
|
|
10
|
-
} from "@doswiftly/storefront-sdk/graphql";
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// TYPES - Using SDK types (SSOT)
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
// Re-export types that might not be directly available
|
|
17
|
-
type PriceRange = ConvertedPriceRange;
|
|
18
|
-
type OriginalPriceRange = ProductPriceRange;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Product variant with price
|
|
22
|
-
*/
|
|
23
|
-
interface ProductVariantPrice {
|
|
24
|
-
id: string;
|
|
25
|
-
title: string;
|
|
26
|
-
price: PriceMoney;
|
|
27
|
-
originalPrice?: Money;
|
|
28
|
-
compareAtPrice?: PriceMoney;
|
|
29
|
-
originalCompareAtPrice?: Money;
|
|
30
|
-
available: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface ProductPriceProps {
|
|
34
|
-
/**
|
|
35
|
-
* Price range from product (converted prices)
|
|
36
|
-
* Use for product listing/cards
|
|
37
|
-
*/
|
|
38
|
-
priceRange?: PriceRange | null;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Original price range (shop base currency)
|
|
42
|
-
* Optional - shown as reference when showOriginal=true
|
|
43
|
-
*/
|
|
44
|
-
originalPriceRange?: OriginalPriceRange | null;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Single variant price (converted)
|
|
48
|
-
* Use for product detail page with selected variant
|
|
49
|
-
*/
|
|
50
|
-
variantPrice?: PriceMoney | null;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Original variant price (shop base currency)
|
|
54
|
-
*/
|
|
55
|
-
originalVariantPrice?: Money | null;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Compare at price (for sales)
|
|
59
|
-
*/
|
|
60
|
-
compareAtPrice?: PriceMoney | null;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Show original price in base currency
|
|
64
|
-
*/
|
|
65
|
-
showOriginal?: boolean;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Show exchange rate info
|
|
69
|
-
*/
|
|
70
|
-
showExchangeRate?: boolean;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Show "from" prefix for price ranges
|
|
74
|
-
*/
|
|
75
|
-
showFromPrefix?: boolean;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Show strikethrough compare at price
|
|
79
|
-
*/
|
|
80
|
-
showCompareAt?: boolean;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Custom className
|
|
84
|
-
*/
|
|
85
|
-
className?: string;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Size variant
|
|
89
|
-
*/
|
|
90
|
-
size?: "sm" | "md" | "lg" | "xl";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ============================================================================
|
|
94
|
-
// COMPONENT
|
|
95
|
-
// ============================================================================
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* ProductPrice - Display product/variant price with currency conversion
|
|
99
|
-
*
|
|
100
|
-
* Works with the new intuitive price API where:
|
|
101
|
-
* - `price` / `priceRange` = converted price (customer's currency)
|
|
102
|
-
* - `originalPrice` / `originalPriceRange` = original price (shop currency)
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```tsx
|
|
106
|
-
* // Product listing - show price range
|
|
107
|
-
* <ProductPrice
|
|
108
|
-
* priceRange={product.priceRange}
|
|
109
|
-
* showFromPrefix
|
|
110
|
-
* />
|
|
111
|
-
*
|
|
112
|
-
* // Product detail - show selected variant price
|
|
113
|
-
* <ProductPrice
|
|
114
|
-
* variantPrice={selectedVariant.price}
|
|
115
|
-
* compareAtPrice={selectedVariant.compareAtPrice}
|
|
116
|
-
* showCompareAt
|
|
117
|
-
* showOriginal
|
|
118
|
-
* size="lg"
|
|
119
|
-
* />
|
|
120
|
-
*
|
|
121
|
-
* // With exchange rate info
|
|
122
|
-
* <ProductPrice
|
|
123
|
-
* variantPrice={variant.price}
|
|
124
|
-
* showExchangeRate
|
|
125
|
-
* />
|
|
126
|
-
* ```
|
|
127
|
-
*/
|
|
128
|
-
export function ProductPrice({
|
|
129
|
-
priceRange,
|
|
130
|
-
originalPriceRange,
|
|
131
|
-
variantPrice,
|
|
132
|
-
originalVariantPrice,
|
|
133
|
-
compareAtPrice,
|
|
134
|
-
showOriginal = false,
|
|
135
|
-
showExchangeRate = false,
|
|
136
|
-
showFromPrefix = false,
|
|
137
|
-
showCompareAt = true,
|
|
138
|
-
className = "",
|
|
139
|
-
size = "md",
|
|
140
|
-
}: ProductPriceProps) {
|
|
141
|
-
// Determine which price to display
|
|
142
|
-
const displayPrice = useMemo(() => {
|
|
143
|
-
if (variantPrice) {
|
|
144
|
-
return variantPrice;
|
|
145
|
-
}
|
|
146
|
-
if (priceRange) {
|
|
147
|
-
return priceRange.minVariantPrice;
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}, [variantPrice, priceRange]);
|
|
151
|
-
|
|
152
|
-
// Determine original price
|
|
153
|
-
const originalPrice = useMemo(() => {
|
|
154
|
-
if (originalVariantPrice) {
|
|
155
|
-
return originalVariantPrice;
|
|
156
|
-
}
|
|
157
|
-
if (originalPriceRange) {
|
|
158
|
-
return originalPriceRange.minVariantPrice;
|
|
159
|
-
}
|
|
160
|
-
return null;
|
|
161
|
-
}, [originalVariantPrice, originalPriceRange]);
|
|
162
|
-
|
|
163
|
-
// Check if price range varies (min != max)
|
|
164
|
-
const hasVariedRange = useMemo(() => {
|
|
165
|
-
if (!priceRange) return false;
|
|
166
|
-
return (
|
|
167
|
-
priceRange.minVariantPrice.amount !== priceRange.maxVariantPrice.amount
|
|
168
|
-
);
|
|
169
|
-
}, [priceRange]);
|
|
170
|
-
|
|
171
|
-
// Check if it's a sale (has compare at price higher than current)
|
|
172
|
-
const isOnSale = useMemo(() => {
|
|
173
|
-
if (!compareAtPrice || !displayPrice) return false;
|
|
174
|
-
return parseFloat(compareAtPrice.amount) > parseFloat(displayPrice.amount);
|
|
175
|
-
}, [compareAtPrice, displayPrice]);
|
|
176
|
-
|
|
177
|
-
// Size styles
|
|
178
|
-
const sizeStyles = {
|
|
179
|
-
sm: "text-sm",
|
|
180
|
-
md: "text-base",
|
|
181
|
-
lg: "text-lg",
|
|
182
|
-
xl: "text-2xl",
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const smallTextStyles = {
|
|
186
|
-
sm: "text-xs",
|
|
187
|
-
md: "text-xs",
|
|
188
|
-
lg: "text-sm",
|
|
189
|
-
xl: "text-base",
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
if (!displayPrice) {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<div className={`flex flex-col gap-0.5 ${className}`}>
|
|
198
|
-
{/* Main price line */}
|
|
199
|
-
<div className="flex items-baseline gap-2 flex-wrap">
|
|
200
|
-
{/* Compare at price (strikethrough) */}
|
|
201
|
-
{showCompareAt && isOnSale && compareAtPrice && (
|
|
202
|
-
<span
|
|
203
|
-
className={`text-gray-400 line-through ${smallTextStyles[size]}`}
|
|
204
|
-
>
|
|
205
|
-
{formatPrice(compareAtPrice)}
|
|
206
|
-
</span>
|
|
207
|
-
)}
|
|
208
|
-
|
|
209
|
-
{/* Current price */}
|
|
210
|
-
<span
|
|
211
|
-
className={`font-semibold ${sizeStyles[size]} ${
|
|
212
|
-
isOnSale
|
|
213
|
-
? "text-red-600 dark:text-red-400"
|
|
214
|
-
: "text-gray-900 dark:text-gray-100"
|
|
215
|
-
}`}
|
|
216
|
-
>
|
|
217
|
-
{showFromPrefix && (hasVariedRange || priceRange) && (
|
|
218
|
-
<span className="font-normal text-gray-500 dark:text-gray-400 mr-1">
|
|
219
|
-
od
|
|
220
|
-
</span>
|
|
221
|
-
)}
|
|
222
|
-
{formatPrice(displayPrice)}
|
|
223
|
-
</span>
|
|
224
|
-
|
|
225
|
-
{/* Sale badge */}
|
|
226
|
-
{isOnSale && (
|
|
227
|
-
<span className="px-1.5 py-0.5 text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 rounded">
|
|
228
|
-
Promocja
|
|
229
|
-
</span>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{/* Price range (if varies) */}
|
|
234
|
-
{hasVariedRange && priceRange && (
|
|
235
|
-
<span
|
|
236
|
-
className={`text-gray-500 dark:text-gray-400 ${smallTextStyles[size]}`}
|
|
237
|
-
>
|
|
238
|
-
{formatPrice(priceRange.minVariantPrice)} –{" "}
|
|
239
|
-
{formatPrice(priceRange.maxVariantPrice)}
|
|
240
|
-
</span>
|
|
241
|
-
)}
|
|
242
|
-
|
|
243
|
-
{/* Original price (in base currency) */}
|
|
244
|
-
{showOriginal && displayPrice.isConverted && originalPrice && (
|
|
245
|
-
<span
|
|
246
|
-
className={`text-gray-400 dark:text-gray-500 ${smallTextStyles[size]}`}
|
|
247
|
-
>
|
|
248
|
-
≈ {formatPrice(originalPrice)}
|
|
249
|
-
</span>
|
|
250
|
-
)}
|
|
251
|
-
|
|
252
|
-
{/* Exchange rate info */}
|
|
253
|
-
{showExchangeRate &&
|
|
254
|
-
displayPrice.isConverted &&
|
|
255
|
-
displayPrice.exchangeRate && (
|
|
256
|
-
<span
|
|
257
|
-
className={`text-gray-400 dark:text-gray-500 ${smallTextStyles[size]}`}
|
|
258
|
-
>
|
|
259
|
-
Kurs: 1 {displayPrice.baseCurrencyCode} ={" "}
|
|
260
|
-
{displayPrice.exchangeRate.toFixed(4)} {displayPrice.currencyCode}
|
|
261
|
-
{displayPrice.marginApplied && (
|
|
262
|
-
<span className="ml-1">
|
|
263
|
-
(+{(displayPrice.marginApplied * 100).toFixed(1)}% marża)
|
|
264
|
-
</span>
|
|
265
|
-
)}
|
|
266
|
-
</span>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ============================================================================
|
|
273
|
-
// VARIANT PRICE SELECTOR
|
|
274
|
-
// ============================================================================
|
|
275
|
-
|
|
276
|
-
interface VariantPriceSelectorProps {
|
|
277
|
-
/** All variants with prices */
|
|
278
|
-
variants: ProductVariantPrice[];
|
|
279
|
-
/** Currently selected variant ID */
|
|
280
|
-
selectedVariantId: string | null;
|
|
281
|
-
/** Callback when variant is selected */
|
|
282
|
-
onSelectVariant: (variantId: string) => void;
|
|
283
|
-
/** Show original prices */
|
|
284
|
-
showOriginal?: boolean;
|
|
285
|
-
/** Layout direction */
|
|
286
|
-
layout?: "horizontal" | "vertical";
|
|
287
|
-
/** Custom className */
|
|
288
|
-
className?: string;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* VariantPriceSelector - Select variant and see its price
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* ```tsx
|
|
296
|
-
* const [selectedId, setSelectedId] = useState(variants[0]?.id);
|
|
297
|
-
*
|
|
298
|
-
* <VariantPriceSelector
|
|
299
|
-
* variants={product.variants}
|
|
300
|
-
* selectedVariantId={selectedId}
|
|
301
|
-
* onSelectVariant={setSelectedId}
|
|
302
|
-
* />
|
|
303
|
-
* ```
|
|
304
|
-
*/
|
|
305
|
-
export function VariantPriceSelector({
|
|
306
|
-
variants,
|
|
307
|
-
selectedVariantId,
|
|
308
|
-
onSelectVariant,
|
|
309
|
-
showOriginal = false,
|
|
310
|
-
layout = "horizontal",
|
|
311
|
-
className = "",
|
|
312
|
-
}: VariantPriceSelectorProps) {
|
|
313
|
-
const selectedVariant = useMemo(
|
|
314
|
-
() => variants.find((v) => v.id === selectedVariantId),
|
|
315
|
-
[variants, selectedVariantId]
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
return (
|
|
319
|
-
<div className={`flex flex-col gap-3 ${className}`}>
|
|
320
|
-
{/* Variant buttons */}
|
|
321
|
-
<div
|
|
322
|
-
className={`flex gap-2 ${
|
|
323
|
-
layout === "vertical" ? "flex-col" : "flex-wrap"
|
|
324
|
-
}`}
|
|
325
|
-
>
|
|
326
|
-
{variants.map((variant) => {
|
|
327
|
-
const isSelected = variant.id === selectedVariantId;
|
|
328
|
-
const isAvailable = variant.available;
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<button
|
|
332
|
-
key={variant.id}
|
|
333
|
-
type="button"
|
|
334
|
-
onClick={() => onSelectVariant(variant.id)}
|
|
335
|
-
disabled={!isAvailable}
|
|
336
|
-
className={`
|
|
337
|
-
px-4 py-2 rounded-lg border text-sm font-medium
|
|
338
|
-
transition-all
|
|
339
|
-
${
|
|
340
|
-
isSelected
|
|
341
|
-
? "border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
|
342
|
-
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
|
343
|
-
}
|
|
344
|
-
${
|
|
345
|
-
!isAvailable
|
|
346
|
-
? "opacity-50 cursor-not-allowed line-through"
|
|
347
|
-
: "cursor-pointer"
|
|
348
|
-
}
|
|
349
|
-
`}
|
|
350
|
-
>
|
|
351
|
-
<span>{variant.title}</span>
|
|
352
|
-
<span className="ml-2 text-gray-500 dark:text-gray-400">
|
|
353
|
-
{formatPrice(variant.price)}
|
|
354
|
-
</span>
|
|
355
|
-
</button>
|
|
356
|
-
);
|
|
357
|
-
})}
|
|
358
|
-
</div>
|
|
359
|
-
|
|
360
|
-
{/* Selected variant price details */}
|
|
361
|
-
{selectedVariant && (
|
|
362
|
-
<ProductPrice
|
|
363
|
-
variantPrice={selectedVariant.price}
|
|
364
|
-
originalVariantPrice={selectedVariant.originalPrice}
|
|
365
|
-
compareAtPrice={selectedVariant.compareAtPrice}
|
|
366
|
-
showOriginal={showOriginal}
|
|
367
|
-
showCompareAt
|
|
368
|
-
size="lg"
|
|
369
|
-
/>
|
|
370
|
-
)}
|
|
371
|
-
</div>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export default ProductPrice;
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
-
import { useRouter } from "next/navigation";
|
|
5
|
-
import { Search, X, Loader2 } from "lucide-react";
|
|
6
|
-
import { useProductSearch } from "@doswiftly/storefront-sdk/graphql/client";
|
|
7
|
-
import { useDebouncedValue } from "@/lib/hooks";
|
|
8
|
-
import Link from "next/link";
|
|
9
|
-
|
|
10
|
-
interface SearchInputProps {
|
|
11
|
-
placeholder?: string;
|
|
12
|
-
className?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* SearchInput - Product search with autocomplete
|
|
17
|
-
*
|
|
18
|
-
* Shows instant search results as user types.
|
|
19
|
-
* Pressing Enter navigates to full search page.
|
|
20
|
-
*/
|
|
21
|
-
export function SearchInput({
|
|
22
|
-
placeholder = "Search products...",
|
|
23
|
-
className = "",
|
|
24
|
-
}: SearchInputProps) {
|
|
25
|
-
const router = useRouter();
|
|
26
|
-
const [query, setQuery] = useState("");
|
|
27
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
29
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
30
|
-
|
|
31
|
-
// Debounce search query
|
|
32
|
-
const debouncedQuery = useDebouncedValue(query, 300);
|
|
33
|
-
|
|
34
|
-
// Fetch search results (client hook - now returns normalized data!)
|
|
35
|
-
const { data, isLoading } = useProductSearch(
|
|
36
|
-
{ query: debouncedQuery, first: 5 },
|
|
37
|
-
{ enabled: debouncedQuery.length >= 2 }
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const products = data?.products ?? [];
|
|
41
|
-
const hasResults = products.length > 0;
|
|
42
|
-
|
|
43
|
-
// Handle form submit
|
|
44
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
if (query.trim()) {
|
|
47
|
-
router.push(`/search?q=${encodeURIComponent(query.trim())}`);
|
|
48
|
-
setIsOpen(false);
|
|
49
|
-
setQuery("");
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Handle click outside
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
56
|
-
if (
|
|
57
|
-
containerRef.current &&
|
|
58
|
-
!containerRef.current.contains(e.target as Node)
|
|
59
|
-
) {
|
|
60
|
-
setIsOpen(false);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
65
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
// Handle escape key
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
71
|
-
if (e.key === "Escape") {
|
|
72
|
-
setIsOpen(false);
|
|
73
|
-
inputRef.current?.blur();
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
document.addEventListener("keydown", handleEscape);
|
|
78
|
-
return () => document.removeEventListener("keydown", handleEscape);
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<div ref={containerRef} className={`relative ${className}`}>
|
|
83
|
-
<form onSubmit={handleSubmit}>
|
|
84
|
-
<div className="relative">
|
|
85
|
-
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
86
|
-
<input
|
|
87
|
-
ref={inputRef}
|
|
88
|
-
type="text"
|
|
89
|
-
value={query}
|
|
90
|
-
onChange={(e) => {
|
|
91
|
-
setQuery(e.target.value);
|
|
92
|
-
setIsOpen(true);
|
|
93
|
-
}}
|
|
94
|
-
onFocus={() => setIsOpen(true)}
|
|
95
|
-
placeholder={placeholder}
|
|
96
|
-
className="input w-full pl-10 pr-10"
|
|
97
|
-
/>
|
|
98
|
-
{query && (
|
|
99
|
-
<button
|
|
100
|
-
type="button"
|
|
101
|
-
onClick={() => {
|
|
102
|
-
setQuery("");
|
|
103
|
-
inputRef.current?.focus();
|
|
104
|
-
}}
|
|
105
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
106
|
-
>
|
|
107
|
-
<X className="h-4 w-4" />
|
|
108
|
-
</button>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
</form>
|
|
112
|
-
|
|
113
|
-
{/* Dropdown Results */}
|
|
114
|
-
{isOpen && query.length >= 2 && (
|
|
115
|
-
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-96 overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg">
|
|
116
|
-
{isLoading ? (
|
|
117
|
-
<div className="flex items-center justify-center py-8">
|
|
118
|
-
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
|
119
|
-
<span className="ml-2 text-sm text-gray-500">Searching...</span>
|
|
120
|
-
</div>
|
|
121
|
-
) : hasResults ? (
|
|
122
|
-
<>
|
|
123
|
-
<ul className="divide-y divide-gray-100">
|
|
124
|
-
{products.slice(0, 5).map((product) => (
|
|
125
|
-
<li key={product.id}>
|
|
126
|
-
<Link
|
|
127
|
-
href={`/products/${product.handle}`}
|
|
128
|
-
onClick={() => {
|
|
129
|
-
setIsOpen(false);
|
|
130
|
-
setQuery("");
|
|
131
|
-
}}
|
|
132
|
-
className="flex items-center gap-4 p-3 hover:bg-gray-50"
|
|
133
|
-
>
|
|
134
|
-
{product.featuredImage?.url ? (
|
|
135
|
-
<img
|
|
136
|
-
src={product.featuredImage.url}
|
|
137
|
-
alt={product.featuredImage.altText || product.title}
|
|
138
|
-
className="h-12 w-12 rounded object-cover"
|
|
139
|
-
/>
|
|
140
|
-
) : (
|
|
141
|
-
<div className="flex h-12 w-12 items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
|
|
142
|
-
No img
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
<div className="flex-1 overflow-hidden">
|
|
146
|
-
<h4 className="truncate font-medium text-gray-900">
|
|
147
|
-
{product.title}
|
|
148
|
-
</h4>
|
|
149
|
-
<p className="text-sm text-primary">
|
|
150
|
-
{product.priceRange?.minVariantPrice?.amount}{" "}
|
|
151
|
-
{product.priceRange?.minVariantPrice?.currencyCode}
|
|
152
|
-
</p>
|
|
153
|
-
</div>
|
|
154
|
-
</Link>
|
|
155
|
-
</li>
|
|
156
|
-
))}
|
|
157
|
-
</ul>
|
|
158
|
-
<Link
|
|
159
|
-
href={`/search?q=${encodeURIComponent(query)}`}
|
|
160
|
-
onClick={() => {
|
|
161
|
-
setIsOpen(false);
|
|
162
|
-
setQuery("");
|
|
163
|
-
}}
|
|
164
|
-
className="block border-t border-gray-100 p-3 text-center text-sm text-primary hover:bg-gray-50"
|
|
165
|
-
>
|
|
166
|
-
View all results →
|
|
167
|
-
</Link>
|
|
168
|
-
</>
|
|
169
|
-
) : (
|
|
170
|
-
<div className="py-8 text-center text-sm text-gray-500">
|
|
171
|
-
No products found for "{query}"
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|