@doswiftly/cli 0.1.24 → 0.2.1
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 +8 -5
- 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,212 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, FormEvent } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { useRouter } from "next/navigation";
|
|
6
|
-
import { useRegister } from "@doswiftly/storefront-sdk/graphql/react";
|
|
7
|
-
import { AuthGuard } from "@/components/auth/auth-guard";
|
|
8
|
-
|
|
9
|
-
function RegisterForm() {
|
|
10
|
-
const router = useRouter();
|
|
11
|
-
const register = useRegister();
|
|
12
|
-
|
|
13
|
-
const [formData, setFormData] = useState({
|
|
14
|
-
firstName: "",
|
|
15
|
-
lastName: "",
|
|
16
|
-
email: "",
|
|
17
|
-
password: "",
|
|
18
|
-
confirmPassword: "",
|
|
19
|
-
});
|
|
20
|
-
const [error, setError] = useState<string | null>(null);
|
|
21
|
-
|
|
22
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
23
|
-
const { name, value, type, checked } = e.target;
|
|
24
|
-
setFormData((prev) => ({
|
|
25
|
-
...prev,
|
|
26
|
-
[name]: type === "checkbox" ? checked : value,
|
|
27
|
-
}));
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const handleSubmit = async (e: FormEvent) => {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
setError(null);
|
|
33
|
-
|
|
34
|
-
// Validate passwords match
|
|
35
|
-
if (formData.password !== formData.confirmPassword) {
|
|
36
|
-
setError("Passwords do not match");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Validate password length
|
|
41
|
-
if (formData.password.length < 8) {
|
|
42
|
-
setError("Password must be at least 8 characters");
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const result = await register.mutateAsync({
|
|
48
|
-
input: {
|
|
49
|
-
email: formData.email,
|
|
50
|
-
password: formData.password,
|
|
51
|
-
firstName: formData.firstName || undefined,
|
|
52
|
-
lastName: formData.lastName || undefined,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Check for user errors
|
|
57
|
-
const userErrors = result.customerCreate?.userErrors;
|
|
58
|
-
if (userErrors && userErrors.length > 0) {
|
|
59
|
-
setError(userErrors[0].message);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Success - redirect to account
|
|
64
|
-
router.push("/account");
|
|
65
|
-
} catch (err) {
|
|
66
|
-
setError("Registration failed. Please try again.");
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className="container mx-auto flex min-h-[60vh] items-center justify-center px-4 py-8">
|
|
72
|
-
<div className="w-full max-w-md">
|
|
73
|
-
<h1 className="mb-8 text-center text-3xl font-bold text-gray-900">
|
|
74
|
-
Create Account
|
|
75
|
-
</h1>
|
|
76
|
-
|
|
77
|
-
{error && (
|
|
78
|
-
<div className="mb-6 rounded-lg bg-red-50 p-4 text-red-700">
|
|
79
|
-
{error}
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
84
|
-
<div className="grid grid-cols-2 gap-4">
|
|
85
|
-
<div>
|
|
86
|
-
<label
|
|
87
|
-
htmlFor="firstName"
|
|
88
|
-
className="mb-2 block text-sm font-medium text-gray-900"
|
|
89
|
-
>
|
|
90
|
-
First Name
|
|
91
|
-
</label>
|
|
92
|
-
<input
|
|
93
|
-
type="text"
|
|
94
|
-
id="firstName"
|
|
95
|
-
name="firstName"
|
|
96
|
-
value={formData.firstName}
|
|
97
|
-
onChange={handleChange}
|
|
98
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-gray-900 focus:outline-none"
|
|
99
|
-
placeholder="John"
|
|
100
|
-
/>
|
|
101
|
-
</div>
|
|
102
|
-
<div>
|
|
103
|
-
<label
|
|
104
|
-
htmlFor="lastName"
|
|
105
|
-
className="mb-2 block text-sm font-medium text-gray-900"
|
|
106
|
-
>
|
|
107
|
-
Last Name
|
|
108
|
-
</label>
|
|
109
|
-
<input
|
|
110
|
-
type="text"
|
|
111
|
-
id="lastName"
|
|
112
|
-
name="lastName"
|
|
113
|
-
value={formData.lastName}
|
|
114
|
-
onChange={handleChange}
|
|
115
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-gray-900 focus:outline-none"
|
|
116
|
-
placeholder="Doe"
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div>
|
|
122
|
-
<label
|
|
123
|
-
htmlFor="email"
|
|
124
|
-
className="mb-2 block text-sm font-medium text-gray-900"
|
|
125
|
-
>
|
|
126
|
-
Email *
|
|
127
|
-
</label>
|
|
128
|
-
<input
|
|
129
|
-
type="email"
|
|
130
|
-
id="email"
|
|
131
|
-
name="email"
|
|
132
|
-
value={formData.email}
|
|
133
|
-
onChange={handleChange}
|
|
134
|
-
required
|
|
135
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-gray-900 focus:outline-none"
|
|
136
|
-
placeholder="your@email.com"
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div>
|
|
141
|
-
<label
|
|
142
|
-
htmlFor="password"
|
|
143
|
-
className="mb-2 block text-sm font-medium text-gray-900"
|
|
144
|
-
>
|
|
145
|
-
Password *
|
|
146
|
-
</label>
|
|
147
|
-
<input
|
|
148
|
-
type="password"
|
|
149
|
-
id="password"
|
|
150
|
-
name="password"
|
|
151
|
-
value={formData.password}
|
|
152
|
-
onChange={handleChange}
|
|
153
|
-
required
|
|
154
|
-
minLength={8}
|
|
155
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-gray-900 focus:outline-none"
|
|
156
|
-
placeholder="••••••••"
|
|
157
|
-
/>
|
|
158
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
159
|
-
Must be at least 8 characters
|
|
160
|
-
</p>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
<div>
|
|
164
|
-
<label
|
|
165
|
-
htmlFor="confirmPassword"
|
|
166
|
-
className="mb-2 block text-sm font-medium text-gray-900"
|
|
167
|
-
>
|
|
168
|
-
Confirm Password *
|
|
169
|
-
</label>
|
|
170
|
-
<input
|
|
171
|
-
type="password"
|
|
172
|
-
id="confirmPassword"
|
|
173
|
-
name="confirmPassword"
|
|
174
|
-
value={formData.confirmPassword}
|
|
175
|
-
onChange={handleChange}
|
|
176
|
-
required
|
|
177
|
-
minLength={8}
|
|
178
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-gray-900 focus:outline-none"
|
|
179
|
-
placeholder="••••••••"
|
|
180
|
-
/>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<button
|
|
184
|
-
type="submit"
|
|
185
|
-
disabled={register.isPending}
|
|
186
|
-
className="w-full rounded-lg bg-gray-900 py-3 font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
187
|
-
>
|
|
188
|
-
{register.isPending ? "Creating account..." : "Create Account"}
|
|
189
|
-
</button>
|
|
190
|
-
</form>
|
|
191
|
-
|
|
192
|
-
<p className="mt-6 text-center text-sm text-gray-600">
|
|
193
|
-
Already have an account?{" "}
|
|
194
|
-
<Link
|
|
195
|
-
href="/auth/login"
|
|
196
|
-
className="font-medium text-gray-900 hover:underline"
|
|
197
|
-
>
|
|
198
|
-
Login
|
|
199
|
-
</Link>
|
|
200
|
-
</p>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export default function RegisterPage() {
|
|
207
|
-
return (
|
|
208
|
-
<AuthGuard requireGuest>
|
|
209
|
-
<RegisterForm />
|
|
210
|
-
</AuthGuard>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import Link from "next/link";
|
|
4
|
-
import { Trash2, Plus, Minus, ShoppingCart, Loader2 } from "lucide-react";
|
|
5
|
-
import { useCartManager } from "@doswiftly/storefront-sdk/graphql/react";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* CartPage - Shopping cart with real GraphQL data
|
|
9
|
-
*
|
|
10
|
-
* Uses useCartManager hook - all cart operations are handled automatically.
|
|
11
|
-
* No manual state management needed!
|
|
12
|
-
*/
|
|
13
|
-
export default function CartPage() {
|
|
14
|
-
const {
|
|
15
|
-
cart,
|
|
16
|
-
lines,
|
|
17
|
-
totalQuantity,
|
|
18
|
-
cartLoading,
|
|
19
|
-
loading,
|
|
20
|
-
removeItem,
|
|
21
|
-
removeItemLoading,
|
|
22
|
-
updateQuantity,
|
|
23
|
-
updateQuantityLoading,
|
|
24
|
-
clearCart,
|
|
25
|
-
} = useCartManager();
|
|
26
|
-
|
|
27
|
-
// Show loading state
|
|
28
|
-
if (cartLoading && !cart) {
|
|
29
|
-
return (
|
|
30
|
-
<div className="container mx-auto px-4 py-16">
|
|
31
|
-
<div className="flex items-center justify-center">
|
|
32
|
-
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
33
|
-
<span className="ml-2 text-gray-600">Loading cart...</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Empty cart state
|
|
40
|
-
if (!cart || totalQuantity === 0) {
|
|
41
|
-
return (
|
|
42
|
-
<div className="container mx-auto px-4 py-8">
|
|
43
|
-
<h1 className="mb-8 text-3xl font-bold text-gray-900">Shopping Cart</h1>
|
|
44
|
-
<div className="py-16 text-center">
|
|
45
|
-
<ShoppingCart className="mx-auto mb-4 h-16 w-16 text-gray-300" />
|
|
46
|
-
<h2 className="mb-2 text-xl font-semibold text-gray-900">
|
|
47
|
-
Your cart is empty
|
|
48
|
-
</h2>
|
|
49
|
-
<p className="mb-8 text-gray-600">
|
|
50
|
-
Looks like you haven't added anything to your cart yet.
|
|
51
|
-
</p>
|
|
52
|
-
<Link href="/products" className="btn btn-primary">
|
|
53
|
-
Continue Shopping
|
|
54
|
-
</Link>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Cart line type from GraphQL
|
|
61
|
-
type CartLine = (typeof lines)[number];
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<div className="container mx-auto px-4 py-8">
|
|
65
|
-
<div className="mb-8 flex items-center justify-between">
|
|
66
|
-
<h1 className="text-3xl font-bold text-gray-900">
|
|
67
|
-
Shopping Cart ({totalQuantity}{" "}
|
|
68
|
-
{totalQuantity === 1 ? "item" : "items"})
|
|
69
|
-
</h1>
|
|
70
|
-
<button
|
|
71
|
-
onClick={clearCart}
|
|
72
|
-
className="text-sm text-gray-500 hover:text-red-500"
|
|
73
|
-
>
|
|
74
|
-
Clear cart
|
|
75
|
-
</button>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div className="grid gap-8 lg:grid-cols-3">
|
|
79
|
-
{/* Cart Items */}
|
|
80
|
-
<div className="lg:col-span-2">
|
|
81
|
-
<div className="space-y-4">
|
|
82
|
-
{(lines as CartLine[]).map((line) => (
|
|
83
|
-
<div key={line.id} className="card flex gap-4">
|
|
84
|
-
{/* Product Image */}
|
|
85
|
-
<div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-lg bg-gray-100">
|
|
86
|
-
{line.merchandise?.image?.url ? (
|
|
87
|
-
<img
|
|
88
|
-
src={line.merchandise.image.url}
|
|
89
|
-
alt={
|
|
90
|
-
line.merchandise.image.altText || line.merchandise.title
|
|
91
|
-
}
|
|
92
|
-
className="h-full w-full object-cover"
|
|
93
|
-
/>
|
|
94
|
-
) : (
|
|
95
|
-
<div className="flex h-full items-center justify-center text-xs text-gray-400">
|
|
96
|
-
No image
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{/* Product Info */}
|
|
102
|
-
<div className="flex flex-1 flex-col">
|
|
103
|
-
<div className="flex justify-between">
|
|
104
|
-
<div>
|
|
105
|
-
<h3 className="font-medium text-gray-900">
|
|
106
|
-
{line.merchandise?.title}
|
|
107
|
-
</h3>
|
|
108
|
-
{/* Selected Options (size, color, etc.) */}
|
|
109
|
-
{line.merchandise?.selectedOptions?.length > 0 && (
|
|
110
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
111
|
-
{line.merchandise.selectedOptions
|
|
112
|
-
.map((opt) => `${opt.name}: ${opt.value}`)
|
|
113
|
-
.join(", ")}
|
|
114
|
-
</p>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
<button
|
|
118
|
-
onClick={() => removeItem(line.id)}
|
|
119
|
-
disabled={removeItemLoading}
|
|
120
|
-
className="text-gray-400 hover:text-red-500 disabled:opacity-50"
|
|
121
|
-
>
|
|
122
|
-
{removeItemLoading ? (
|
|
123
|
-
<Loader2 className="h-5 w-5 animate-spin" />
|
|
124
|
-
) : (
|
|
125
|
-
<Trash2 className="h-5 w-5" />
|
|
126
|
-
)}
|
|
127
|
-
</button>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div className="mt-auto flex items-center justify-between">
|
|
131
|
-
{/* Quantity Controls */}
|
|
132
|
-
<div className="flex items-center rounded-lg border border-gray-300">
|
|
133
|
-
<button
|
|
134
|
-
onClick={() =>
|
|
135
|
-
updateQuantity(line.id, line.quantity - 1)
|
|
136
|
-
}
|
|
137
|
-
disabled={updateQuantityLoading || line.quantity <= 1}
|
|
138
|
-
className="p-2 text-gray-600 hover:text-primary disabled:opacity-50"
|
|
139
|
-
>
|
|
140
|
-
<Minus className="h-4 w-4" />
|
|
141
|
-
</button>
|
|
142
|
-
<span className="w-12 text-center">
|
|
143
|
-
{updateQuantityLoading ? (
|
|
144
|
-
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
|
145
|
-
) : (
|
|
146
|
-
line.quantity
|
|
147
|
-
)}
|
|
148
|
-
</span>
|
|
149
|
-
<button
|
|
150
|
-
onClick={() =>
|
|
151
|
-
updateQuantity(line.id, line.quantity + 1)
|
|
152
|
-
}
|
|
153
|
-
disabled={updateQuantityLoading}
|
|
154
|
-
className="p-2 text-gray-600 hover:text-primary disabled:opacity-50"
|
|
155
|
-
>
|
|
156
|
-
<Plus className="h-4 w-4" />
|
|
157
|
-
</button>
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
{/* Line Total */}
|
|
161
|
-
<span className="font-semibold text-gray-900">
|
|
162
|
-
{line.cost?.totalAmount?.amount}{" "}
|
|
163
|
-
{line.cost?.totalAmount?.currencyCode}
|
|
164
|
-
</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
{/* Order Summary */}
|
|
173
|
-
<div className="lg:col-span-1">
|
|
174
|
-
<div className="card sticky top-24">
|
|
175
|
-
<h2 className="mb-4 text-lg font-semibold text-gray-900">
|
|
176
|
-
Order Summary
|
|
177
|
-
</h2>
|
|
178
|
-
|
|
179
|
-
<div className="space-y-3 text-sm">
|
|
180
|
-
<div className="flex justify-between">
|
|
181
|
-
<span className="text-gray-600">Subtotal</span>
|
|
182
|
-
<span className="text-gray-900">
|
|
183
|
-
{cart.cost?.subtotalAmount?.amount}{" "}
|
|
184
|
-
{cart.cost?.subtotalAmount?.currencyCode}
|
|
185
|
-
</span>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
{cart.cost?.totalTaxAmount && (
|
|
189
|
-
<div className="flex justify-between">
|
|
190
|
-
<span className="text-gray-600">Tax</span>
|
|
191
|
-
<span className="text-gray-900">
|
|
192
|
-
{cart.cost.totalTaxAmount.amount}{" "}
|
|
193
|
-
{cart.cost.totalTaxAmount.currencyCode}
|
|
194
|
-
</span>
|
|
195
|
-
</div>
|
|
196
|
-
)}
|
|
197
|
-
|
|
198
|
-
<div className="flex justify-between">
|
|
199
|
-
<span className="text-gray-600">Shipping</span>
|
|
200
|
-
<span className="text-gray-500 italic">
|
|
201
|
-
Calculated at checkout
|
|
202
|
-
</span>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
{/* Discount Codes */}
|
|
206
|
-
{cart.discountCodes?.length > 0 && (
|
|
207
|
-
<div className="border-t border-gray-200 pt-3">
|
|
208
|
-
<span className="text-xs font-medium uppercase text-gray-500">
|
|
209
|
-
Applied Discounts
|
|
210
|
-
</span>
|
|
211
|
-
{cart.discountCodes.map((discount) => (
|
|
212
|
-
<div
|
|
213
|
-
key={discount.code}
|
|
214
|
-
className="mt-1 flex items-center justify-between"
|
|
215
|
-
>
|
|
216
|
-
<span className="text-green-600">{discount.code}</span>
|
|
217
|
-
{!discount.applicable && (
|
|
218
|
-
<span className="text-xs text-red-500">
|
|
219
|
-
Not applicable
|
|
220
|
-
</span>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
))}
|
|
224
|
-
</div>
|
|
225
|
-
)}
|
|
226
|
-
|
|
227
|
-
<div className="border-t border-gray-200 pt-3">
|
|
228
|
-
<div className="flex justify-between font-semibold">
|
|
229
|
-
<span className="text-gray-900">Total</span>
|
|
230
|
-
<span className="text-primary">
|
|
231
|
-
{cart.cost?.totalAmount?.amount}{" "}
|
|
232
|
-
{cart.cost?.totalAmount?.currencyCode}
|
|
233
|
-
</span>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
|
|
238
|
-
{/* Checkout Button */}
|
|
239
|
-
{cart.checkoutUrl ? (
|
|
240
|
-
<a
|
|
241
|
-
href={cart.checkoutUrl}
|
|
242
|
-
className="btn btn-primary mt-6 w-full"
|
|
243
|
-
>
|
|
244
|
-
{loading ? (
|
|
245
|
-
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
246
|
-
) : null}
|
|
247
|
-
Proceed to Checkout
|
|
248
|
-
</a>
|
|
249
|
-
) : (
|
|
250
|
-
<Link href="/checkout" className="btn btn-primary mt-6 w-full">
|
|
251
|
-
Proceed to Checkout
|
|
252
|
-
</Link>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
<Link href="/products" className="btn btn-outline mt-3 w-full">
|
|
256
|
-
Continue Shopping
|
|
257
|
-
</Link>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import {
|
|
4
|
-
useCategory,
|
|
5
|
-
useProducts,
|
|
6
|
-
} from "@doswiftly/storefront-sdk/graphql/server";
|
|
7
|
-
import { ProductSortKeys } from "@doswiftly/storefront-sdk/graphql";
|
|
8
|
-
import { Pagination } from "@/components/commerce/pagination";
|
|
9
|
-
import { SortSelect } from "@/components/commerce/sort-select";
|
|
10
|
-
|
|
11
|
-
interface CategoryPageProps {
|
|
12
|
-
params: Promise<{ slug: string }>;
|
|
13
|
-
searchParams: Promise<{
|
|
14
|
-
after?: string;
|
|
15
|
-
before?: string;
|
|
16
|
-
sort?: string;
|
|
17
|
-
order?: string;
|
|
18
|
-
}>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export default async function CategoryPage({
|
|
22
|
-
params,
|
|
23
|
-
searchParams,
|
|
24
|
-
}: CategoryPageProps) {
|
|
25
|
-
const { slug } = await params;
|
|
26
|
-
const { after, sort, order } = await searchParams;
|
|
27
|
-
|
|
28
|
-
// Fetch category details by slug
|
|
29
|
-
const categoryData = await useCategory({ slug });
|
|
30
|
-
const category = categoryData.category;
|
|
31
|
-
|
|
32
|
-
if (!category) {
|
|
33
|
-
notFound();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Build sort options from URL params
|
|
37
|
-
const sortKey =
|
|
38
|
-
(sort as keyof typeof ProductSortKeys) || ProductSortKeys.Relevance;
|
|
39
|
-
const reverse = order === "asc" ? false : true;
|
|
40
|
-
|
|
41
|
-
// Fetch products for this category using query filter
|
|
42
|
-
const { products, pageInfo } = await useProducts({
|
|
43
|
-
first: 12,
|
|
44
|
-
after,
|
|
45
|
-
query: `category:${category.slug}`, // Filter by category slug
|
|
46
|
-
sortKey: ProductSortKeys[sortKey as keyof typeof ProductSortKeys],
|
|
47
|
-
reverse,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="container mx-auto px-4 py-8">
|
|
52
|
-
{/* Breadcrumb */}
|
|
53
|
-
<nav className="mb-4 text-sm">
|
|
54
|
-
<ol className="flex items-center gap-2">
|
|
55
|
-
<li>
|
|
56
|
-
<Link href="/" className="text-gray-500 hover:text-gray-700">
|
|
57
|
-
Home
|
|
58
|
-
</Link>
|
|
59
|
-
</li>
|
|
60
|
-
<li className="text-gray-400">/</li>
|
|
61
|
-
<li>
|
|
62
|
-
<Link
|
|
63
|
-
href="/categories"
|
|
64
|
-
className="text-gray-500 hover:text-gray-700"
|
|
65
|
-
>
|
|
66
|
-
Categories
|
|
67
|
-
</Link>
|
|
68
|
-
</li>
|
|
69
|
-
{category.parent && (
|
|
70
|
-
<>
|
|
71
|
-
<li className="text-gray-400">/</li>
|
|
72
|
-
<li>
|
|
73
|
-
<Link
|
|
74
|
-
href={`/categories/${category.parent.slug}`}
|
|
75
|
-
className="text-gray-500 hover:text-gray-700"
|
|
76
|
-
>
|
|
77
|
-
{category.parent.name}
|
|
78
|
-
</Link>
|
|
79
|
-
</li>
|
|
80
|
-
</>
|
|
81
|
-
)}
|
|
82
|
-
<li className="text-gray-400">/</li>
|
|
83
|
-
<li className="text-gray-900">{category.name}</li>
|
|
84
|
-
</ol>
|
|
85
|
-
</nav>
|
|
86
|
-
|
|
87
|
-
{/* Category Header */}
|
|
88
|
-
<div className="mb-8">
|
|
89
|
-
<h1 className="text-3xl font-bold text-gray-900">{category.name}</h1>
|
|
90
|
-
{category.description && (
|
|
91
|
-
<p className="mt-2 text-gray-600">{category.description}</p>
|
|
92
|
-
)}
|
|
93
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
94
|
-
{category.productCount}{" "}
|
|
95
|
-
{category.productCount === 1 ? "product" : "products"}
|
|
96
|
-
</p>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Subcategories */}
|
|
100
|
-
{category.children && category.children.length > 0 && (
|
|
101
|
-
<div className="mb-8">
|
|
102
|
-
<h2 className="mb-4 text-lg font-semibold text-gray-900">
|
|
103
|
-
Subcategories
|
|
104
|
-
</h2>
|
|
105
|
-
<div className="flex flex-wrap gap-2">
|
|
106
|
-
{category.children.map(
|
|
107
|
-
(child: { id: string; name: string; slug: string }) => (
|
|
108
|
-
<Link
|
|
109
|
-
key={child.id}
|
|
110
|
-
href={`/categories/${child.slug}`}
|
|
111
|
-
className="rounded-full border border-gray-200 px-4 py-2 text-sm text-gray-700 hover:border-gray-900 hover:text-gray-900"
|
|
112
|
-
>
|
|
113
|
-
{child.name}
|
|
114
|
-
</Link>
|
|
115
|
-
)
|
|
116
|
-
)}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
{/* Sort Options */}
|
|
122
|
-
<div className="mb-6 flex items-center justify-between">
|
|
123
|
-
<p className="text-sm text-gray-500">
|
|
124
|
-
Showing {products.length} of {category.productCount} products
|
|
125
|
-
</p>
|
|
126
|
-
<SortSelect currentSort={sort} currentOrder={order} />
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{/* Products Grid */}
|
|
130
|
-
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 lg:gap-6">
|
|
131
|
-
{products.length === 0 ? (
|
|
132
|
-
<div className="col-span-full py-12 text-center text-gray-500">
|
|
133
|
-
No products in this category
|
|
134
|
-
</div>
|
|
135
|
-
) : (
|
|
136
|
-
products.map((product) => {
|
|
137
|
-
const price = product.priceRange?.minVariantPrice;
|
|
138
|
-
const compareAtPrice = product.compareAtPriceRange?.minVariantPrice;
|
|
139
|
-
const hasDiscount =
|
|
140
|
-
compareAtPrice &&
|
|
141
|
-
price &&
|
|
142
|
-
parseFloat(compareAtPrice.amount) > parseFloat(price.amount);
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<Link
|
|
146
|
-
key={product.id}
|
|
147
|
-
href={`/products/${product.handle}`}
|
|
148
|
-
className="group"
|
|
149
|
-
>
|
|
150
|
-
<div className="relative aspect-square overflow-hidden rounded-lg bg-gray-100">
|
|
151
|
-
{product.featuredImage?.url ? (
|
|
152
|
-
<img
|
|
153
|
-
src={product.featuredImage.url}
|
|
154
|
-
alt={product.featuredImage.altText || product.title}
|
|
155
|
-
className="h-full w-full object-cover transition group-hover:scale-105"
|
|
156
|
-
/>
|
|
157
|
-
) : (
|
|
158
|
-
<div className="flex h-full items-center justify-center">
|
|
159
|
-
<span className="text-4xl text-gray-400">📷</span>
|
|
160
|
-
</div>
|
|
161
|
-
)}
|
|
162
|
-
{hasDiscount && (
|
|
163
|
-
<span className="absolute left-2 top-2 rounded bg-red-500 px-2 py-1 text-xs font-medium text-white">
|
|
164
|
-
Sale
|
|
165
|
-
</span>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
<div className="mt-3">
|
|
169
|
-
<h3 className="text-sm font-medium text-gray-900 group-hover:text-gray-700">
|
|
170
|
-
{product.title}
|
|
171
|
-
</h3>
|
|
172
|
-
{price && (
|
|
173
|
-
<div className="mt-1 flex items-center gap-2">
|
|
174
|
-
<span className="text-sm font-medium text-gray-900">
|
|
175
|
-
{price.currencyCode} {price.amount}
|
|
176
|
-
</span>
|
|
177
|
-
{hasDiscount && compareAtPrice && (
|
|
178
|
-
<span className="text-sm text-gray-500 line-through">
|
|
179
|
-
{compareAtPrice.currencyCode} {compareAtPrice.amount}
|
|
180
|
-
</span>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
</Link>
|
|
186
|
-
);
|
|
187
|
-
})
|
|
188
|
-
)}
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Pagination */}
|
|
192
|
-
<Pagination
|
|
193
|
-
hasMore={pageInfo.hasNextPage}
|
|
194
|
-
endCursor={pageInfo.endCursor}
|
|
195
|
-
currentCursor={after}
|
|
196
|
-
totalShown={products.length}
|
|
197
|
-
/>
|
|
198
|
-
</div>
|
|
199
|
-
);
|
|
200
|
-
}
|