create-brainerce-store 1.0.0 → 1.1.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/index.js +2 -2
- package/package.json +44 -44
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +13 -24
- package/templates/nextjs/base/src/app/cart/page.tsx +16 -170
- package/templates/nextjs/base/src/app/checkout/page.tsx +477 -463
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +1 -1
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +302 -273
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +8 -30
package/dist/index.js
CHANGED
|
@@ -31,10 +31,10 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.1.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
|
-
"create-brainerce-store": "
|
|
37
|
+
"create-brainerce-store": "dist/index.js"
|
|
38
38
|
},
|
|
39
39
|
files: [
|
|
40
40
|
"dist",
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-brainerce-store",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
-
"bin": {
|
|
6
|
-
"create-brainerce-store": "dist/index.js"
|
|
7
|
-
},
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"templates"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
14
|
-
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
15
|
-
"clean": "rimraf dist"
|
|
16
|
-
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"chalk": "^4.1.2",
|
|
19
|
-
"commander": "^12.1.0",
|
|
20
|
-
"ejs": "^3.1.10",
|
|
21
|
-
"fs-extra": "^11.2.0",
|
|
22
|
-
"ora": "^5.4.1",
|
|
23
|
-
"prompts": "^2.4.2"
|
|
24
|
-
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"@types/ejs": "^3.1.5",
|
|
27
|
-
"@types/fs-extra": "^11.0.4",
|
|
28
|
-
"@types/prompts": "^2.4.9",
|
|
29
|
-
"tsup": "^8.0.0",
|
|
30
|
-
"typescript": "^5.4.0"
|
|
31
|
-
},
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=18"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"brainerce",
|
|
37
|
-
"ecommerce",
|
|
38
|
-
"storefront",
|
|
39
|
-
"scaffold",
|
|
40
|
-
"create",
|
|
41
|
-
"nextjs"
|
|
42
|
-
],
|
|
43
|
-
"license": "MIT"
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "create-brainerce-store",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-brainerce-store": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"templates"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
14
|
+
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
15
|
+
"clean": "rimraf dist"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^4.1.2",
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"ejs": "^3.1.10",
|
|
21
|
+
"fs-extra": "^11.2.0",
|
|
22
|
+
"ora": "^5.4.1",
|
|
23
|
+
"prompts": "^2.4.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/ejs": "^3.1.5",
|
|
27
|
+
"@types/fs-extra": "^11.0.4",
|
|
28
|
+
"@types/prompts": "^2.4.9",
|
|
29
|
+
"tsup": "^8.0.0",
|
|
30
|
+
"typescript": "^5.4.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"brainerce",
|
|
37
|
+
"ecommerce",
|
|
38
|
+
"storefront",
|
|
39
|
+
"scaffold",
|
|
40
|
+
"create",
|
|
41
|
+
"nextjs"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { Suspense, useEffect, useState, useRef } from 'react';
|
|
4
4
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
-
import type { CustomerOAuthProvider } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
5
|
import { useAuth } from '@/providers/store-provider';
|
|
8
6
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
7
|
|
|
@@ -14,36 +12,27 @@ function OAuthCallbackContent() {
|
|
|
14
12
|
const [error, setError] = useState<string | null>(null);
|
|
15
13
|
const processedRef = useRef(false);
|
|
16
14
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
15
|
+
const oauthSuccess = searchParams.get('oauth_success');
|
|
16
|
+
const token = searchParams.get('token');
|
|
17
|
+
const oauthError = searchParams.get('oauth_error');
|
|
20
18
|
|
|
21
19
|
useEffect(() => {
|
|
22
20
|
// Prevent double-processing in React StrictMode
|
|
23
21
|
if (processedRef.current) return;
|
|
24
22
|
processedRef.current = true;
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const client = getClient();
|
|
34
|
-
const result = await client.handleOAuthCallback(provider, code, state || '');
|
|
35
|
-
|
|
36
|
-
auth.login(result.token);
|
|
37
|
-
router.push('/');
|
|
38
|
-
} catch (err) {
|
|
39
|
-
const message =
|
|
40
|
-
err instanceof Error ? err.message : 'Authentication failed. Please try again.';
|
|
41
|
-
setError(message);
|
|
42
|
-
}
|
|
24
|
+
if (oauthError) {
|
|
25
|
+
setError(oauthError);
|
|
26
|
+
return;
|
|
43
27
|
}
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
if (oauthSuccess === 'true' && token) {
|
|
30
|
+
auth.login(token);
|
|
31
|
+
router.push('/');
|
|
32
|
+
} else {
|
|
33
|
+
setError('Missing authentication parameters. Please try again.');
|
|
34
|
+
}
|
|
35
|
+
}, [oauthSuccess, token, oauthError, auth, router]);
|
|
47
36
|
|
|
48
37
|
if (error) {
|
|
49
38
|
return (
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
4
3
|
import Link from 'next/link';
|
|
5
|
-
import
|
|
6
|
-
import type { LocalCart, LocalCartItem } from 'brainerce';
|
|
7
|
-
import { formatPrice } from 'brainerce';
|
|
8
|
-
import { useStoreInfo, useCart } from '@/providers/store-provider';
|
|
4
|
+
import { useCart } from '@/providers/store-provider';
|
|
9
5
|
import { CartItem } from '@/components/cart/cart-item';
|
|
10
6
|
import { CartSummary } from '@/components/cart/cart-summary';
|
|
11
7
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
@@ -14,9 +10,7 @@ import { ReservationCountdown } from '@/components/cart/reservation-countdown';
|
|
|
14
10
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
15
11
|
|
|
16
12
|
export default function CartPage() {
|
|
17
|
-
const {
|
|
18
|
-
const { cart, cartLoading, refreshCart, itemCount, isServerCart } = useCart();
|
|
19
|
-
const currency = storeInfo?.currency || 'USD';
|
|
13
|
+
const { cart, cartLoading, refreshCart, itemCount } = useCart();
|
|
20
14
|
|
|
21
15
|
if (cartLoading) {
|
|
22
16
|
return (
|
|
@@ -57,8 +51,6 @@ export default function CartPage() {
|
|
|
57
51
|
);
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
const serverCart = isServerCart(cart) ? cart : null;
|
|
61
|
-
|
|
62
54
|
return (
|
|
63
55
|
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
64
56
|
<h1 className="text-foreground mb-6 text-2xl font-bold">
|
|
@@ -66,47 +58,29 @@ export default function CartPage() {
|
|
|
66
58
|
</h1>
|
|
67
59
|
|
|
68
60
|
{/* Reservation countdown */}
|
|
69
|
-
{
|
|
70
|
-
<ReservationCountdown reservation={
|
|
61
|
+
{cart.reservation?.hasReservation && (
|
|
62
|
+
<ReservationCountdown reservation={cart.reservation} className="mb-6" />
|
|
71
63
|
)}
|
|
72
64
|
|
|
73
65
|
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
|
74
66
|
{/* Cart Items */}
|
|
75
67
|
<div className="lg:col-span-2">
|
|
76
68
|
{/* Nudges */}
|
|
77
|
-
{
|
|
78
|
-
<CartNudges nudges={
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
{/* Server cart items */}
|
|
82
|
-
{serverCart && (
|
|
83
|
-
<div>
|
|
84
|
-
{serverCart.items.map((item) => (
|
|
85
|
-
<CartItem key={item.id} item={item} onUpdate={refreshCart} />
|
|
86
|
-
))}
|
|
87
|
-
</div>
|
|
69
|
+
{cart.nudges && cart.nudges.length > 0 && (
|
|
70
|
+
<CartNudges nudges={cart.nudges} className="mb-4" />
|
|
88
71
|
)}
|
|
89
72
|
|
|
90
|
-
{/*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
item={item}
|
|
97
|
-
currency={currency}
|
|
98
|
-
onUpdate={refreshCart}
|
|
99
|
-
/>
|
|
100
|
-
))}
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
73
|
+
{/* Cart items */}
|
|
74
|
+
<div>
|
|
75
|
+
{cart.items.map((item) => (
|
|
76
|
+
<CartItem key={item.id} item={item} onUpdate={refreshCart} />
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
103
79
|
|
|
104
|
-
{/* Coupon input
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
80
|
+
{/* Coupon input */}
|
|
81
|
+
<div className="border-border mt-6 border-t pt-4">
|
|
82
|
+
<CouponInput cart={cart} onUpdate={refreshCart} />
|
|
83
|
+
</div>
|
|
110
84
|
</div>
|
|
111
85
|
|
|
112
86
|
{/* Summary sidebar */}
|
|
@@ -133,131 +107,3 @@ export default function CartPage() {
|
|
|
133
107
|
</div>
|
|
134
108
|
);
|
|
135
109
|
}
|
|
136
|
-
|
|
137
|
-
// Local cart item display for guest users
|
|
138
|
-
function LocalCartItemRow({
|
|
139
|
-
item,
|
|
140
|
-
currency,
|
|
141
|
-
onUpdate,
|
|
142
|
-
}: {
|
|
143
|
-
item: LocalCartItem;
|
|
144
|
-
currency: string;
|
|
145
|
-
onUpdate: () => void;
|
|
146
|
-
}) {
|
|
147
|
-
const [updating, setUpdating] = useState(false);
|
|
148
|
-
const [removing, setRemoving] = useState(false);
|
|
149
|
-
|
|
150
|
-
const unitPrice = parseFloat(item.price || '0');
|
|
151
|
-
const lineTotal = unitPrice * item.quantity;
|
|
152
|
-
|
|
153
|
-
async function handleQuantityChange(newQuantity: number) {
|
|
154
|
-
if (newQuantity < 1 || updating) return;
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
setUpdating(true);
|
|
158
|
-
const { getClient } = await import('@/lib/brainerce');
|
|
159
|
-
const client = getClient();
|
|
160
|
-
await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId);
|
|
161
|
-
onUpdate();
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.error('Failed to update quantity:', err);
|
|
164
|
-
} finally {
|
|
165
|
-
setUpdating(false);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function handleRemove() {
|
|
170
|
-
if (removing) return;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
setRemoving(true);
|
|
174
|
-
const { getClient } = await import('@/lib/brainerce');
|
|
175
|
-
const client = getClient();
|
|
176
|
-
await client.smartRemoveFromCart(item.productId, item.variantId);
|
|
177
|
-
onUpdate();
|
|
178
|
-
} catch (err) {
|
|
179
|
-
console.error('Failed to remove item:', err);
|
|
180
|
-
} finally {
|
|
181
|
-
setRemoving(false);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<div className="border-border flex gap-4 border-b py-4 last:border-0">
|
|
187
|
-
{/* Image */}
|
|
188
|
-
<div className="bg-muted relative h-20 w-20 flex-shrink-0 overflow-hidden rounded">
|
|
189
|
-
{item.image ? (
|
|
190
|
-
<Image
|
|
191
|
-
src={item.image}
|
|
192
|
-
alt={item.name || 'Product'}
|
|
193
|
-
fill
|
|
194
|
-
sizes="80px"
|
|
195
|
-
className="object-cover"
|
|
196
|
-
/>
|
|
197
|
-
) : (
|
|
198
|
-
<div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
|
|
199
|
-
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
200
|
-
<path
|
|
201
|
-
strokeLinecap="round"
|
|
202
|
-
strokeLinejoin="round"
|
|
203
|
-
strokeWidth={1.5}
|
|
204
|
-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
205
|
-
/>
|
|
206
|
-
</svg>
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{/* Details */}
|
|
212
|
-
<div className="min-w-0 flex-1">
|
|
213
|
-
<h3 className="text-foreground truncate text-sm font-medium">{item.name || 'Product'}</h3>
|
|
214
|
-
|
|
215
|
-
<p className="text-muted-foreground mt-1 text-sm">
|
|
216
|
-
{formatPrice(unitPrice, { currency }) as string}
|
|
217
|
-
</p>
|
|
218
|
-
|
|
219
|
-
<div className="mt-2 flex items-center gap-3">
|
|
220
|
-
<div className="border-border flex items-center rounded border">
|
|
221
|
-
<button
|
|
222
|
-
type="button"
|
|
223
|
-
onClick={() => handleQuantityChange(item.quantity - 1)}
|
|
224
|
-
disabled={updating || item.quantity <= 1}
|
|
225
|
-
className="text-foreground hover:bg-muted px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
|
226
|
-
aria-label="Decrease quantity"
|
|
227
|
-
>
|
|
228
|
-
-
|
|
229
|
-
</button>
|
|
230
|
-
<span className="text-foreground min-w-[2.5rem] px-3 py-1 text-center text-sm font-medium">
|
|
231
|
-
{item.quantity}
|
|
232
|
-
</span>
|
|
233
|
-
<button
|
|
234
|
-
type="button"
|
|
235
|
-
onClick={() => handleQuantityChange(item.quantity + 1)}
|
|
236
|
-
disabled={updating}
|
|
237
|
-
className="text-foreground hover:bg-muted px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
|
238
|
-
aria-label="Increase quantity"
|
|
239
|
-
>
|
|
240
|
-
+
|
|
241
|
-
</button>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<button
|
|
245
|
-
type="button"
|
|
246
|
-
onClick={handleRemove}
|
|
247
|
-
disabled={removing}
|
|
248
|
-
className="text-destructive hover:text-destructive/80 text-xs transition-colors disabled:opacity-40"
|
|
249
|
-
>
|
|
250
|
-
{removing ? 'Removing...' : 'Remove'}
|
|
251
|
-
</button>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Line total */}
|
|
256
|
-
<div className="flex-shrink-0 text-end">
|
|
257
|
-
<span className="text-foreground text-sm font-medium">
|
|
258
|
-
{formatPrice(lineTotal, { currency }) as string}
|
|
259
|
-
</span>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
);
|
|
263
|
-
}
|