create-brainerce-store 1.9.0 → 1.11.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 +1 -1
- package/messages/en.json +12 -2
- package/messages/he.json +12 -2
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +6 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +53 -10
- package/templates/nextjs/base/src/components/account/order-history.tsx +73 -2
- package/templates/nextjs/base/src/components/products/product-card.tsx +6 -0
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +2 -1
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ 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.11.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
|
@@ -77,7 +77,11 @@
|
|
|
77
77
|
"increaseQuantity": "Increase quantity",
|
|
78
78
|
"youMayAlsoLike": "You May Also Like",
|
|
79
79
|
"similarProducts": "Similar Products",
|
|
80
|
-
"upgradeYourChoice": "Upgrade Your Choice"
|
|
80
|
+
"upgradeYourChoice": "Upgrade Your Choice",
|
|
81
|
+
"digitalProduct": "Digital Product",
|
|
82
|
+
"instantDownload": "Instant Download",
|
|
83
|
+
"downloadAfterPurchase": "Available for download after purchase",
|
|
84
|
+
"filesIncluded": "{count} files included"
|
|
81
85
|
},
|
|
82
86
|
"cart": {
|
|
83
87
|
"pageTitle": "Shopping Cart",
|
|
@@ -261,7 +265,13 @@
|
|
|
261
265
|
"save": "Save",
|
|
262
266
|
"cancel": "Cancel",
|
|
263
267
|
"profileUpdated": "Profile updated successfully",
|
|
264
|
-
"profileUpdateFailed": "Failed to update profile"
|
|
268
|
+
"profileUpdateFailed": "Failed to update profile",
|
|
269
|
+
"downloads": "Downloads",
|
|
270
|
+
"downloadFile": "Download",
|
|
271
|
+
"downloadsRemaining": "{used} of {limit} downloads used",
|
|
272
|
+
"unlimitedDownloads": "Unlimited downloads",
|
|
273
|
+
"expiresAt": "Expires {date}",
|
|
274
|
+
"noExpiry": "No expiry"
|
|
265
275
|
},
|
|
266
276
|
"orderConfirmation": {
|
|
267
277
|
"pageTitle": "Order Confirmation",
|
package/messages/he.json
CHANGED
|
@@ -77,7 +77,11 @@
|
|
|
77
77
|
"increaseQuantity": "הגדל כמות",
|
|
78
78
|
"youMayAlsoLike": "אולי גם תאהבו",
|
|
79
79
|
"similarProducts": "מוצרים דומים",
|
|
80
|
-
"upgradeYourChoice": "שדרגו את הבחירה"
|
|
80
|
+
"upgradeYourChoice": "שדרגו את הבחירה",
|
|
81
|
+
"digitalProduct": "מוצר דיגיטלי",
|
|
82
|
+
"instantDownload": "הורדה מיידית",
|
|
83
|
+
"downloadAfterPurchase": "זמין להורדה לאחר רכישה",
|
|
84
|
+
"filesIncluded": "{count} קבצים כלולים"
|
|
81
85
|
},
|
|
82
86
|
"cart": {
|
|
83
87
|
"pageTitle": "עגלת קניות",
|
|
@@ -261,7 +265,13 @@
|
|
|
261
265
|
"save": "שמירה",
|
|
262
266
|
"cancel": "ביטול",
|
|
263
267
|
"profileUpdated": "הפרופיל עודכן בהצלחה",
|
|
264
|
-
"profileUpdateFailed": "עדכון הפרופיל נכשל"
|
|
268
|
+
"profileUpdateFailed": "עדכון הפרופיל נכשל",
|
|
269
|
+
"downloads": "הורדות",
|
|
270
|
+
"downloadFile": "הורד",
|
|
271
|
+
"downloadsRemaining": "{used} מתוך {limit} הורדות נוצלו",
|
|
272
|
+
"unlimitedDownloads": "הורדות ללא הגבלה",
|
|
273
|
+
"expiresAt": "פג תוקף {date}",
|
|
274
|
+
"noExpiry": "ללא תפוגה"
|
|
265
275
|
},
|
|
266
276
|
"orderConfirmation": {
|
|
267
277
|
"pageTitle": "אישור הזמנה",
|
package/package.json
CHANGED
|
@@ -74,6 +74,12 @@ async function proxyRequest(
|
|
|
74
74
|
'Content-Type': 'application/json',
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
// Forward Origin/Referer so backend BrowserOriginGuard accepts proxied requests
|
|
78
|
+
const origin = request.headers.get('origin');
|
|
79
|
+
const referer = request.headers.get('referer');
|
|
80
|
+
if (origin) headers['Origin'] = origin;
|
|
81
|
+
if (referer) headers['Referer'] = referer;
|
|
82
|
+
|
|
77
83
|
// Forward SDK version header if present
|
|
78
84
|
const sdkVersion = request.headers.get('x-sdk-version');
|
|
79
85
|
if (sdkVersion) {
|
|
@@ -10,7 +10,12 @@ import type {
|
|
|
10
10
|
ProductImage,
|
|
11
11
|
ProductMetafield,
|
|
12
12
|
ProductRecommendationsResponse,
|
|
13
|
+
DownloadFile,
|
|
13
14
|
} from 'brainerce';
|
|
15
|
+
|
|
16
|
+
type ProductWithRecommendations = Product & {
|
|
17
|
+
recommendations?: ProductRecommendationsResponse;
|
|
18
|
+
};
|
|
14
19
|
import { getProductPriceInfo, getDescriptionContent } from 'brainerce';
|
|
15
20
|
import { getClient } from '@/lib/brainerce';
|
|
16
21
|
import { useCart } from '@/providers/store-provider';
|
|
@@ -112,7 +117,7 @@ export default function ProductDetailPage() {
|
|
|
112
117
|
const { refreshCart } = useCart();
|
|
113
118
|
const t = useTranslations('productDetail');
|
|
114
119
|
const tc = useTranslations('common');
|
|
115
|
-
const [product, setProduct] = useState<
|
|
120
|
+
const [product, setProduct] = useState<ProductWithRecommendations | null>(null);
|
|
116
121
|
const [loading, setLoading] = useState(true);
|
|
117
122
|
const [error, setError] = useState<string | null>(null);
|
|
118
123
|
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
|
@@ -120,9 +125,8 @@ export default function ProductDetailPage() {
|
|
|
120
125
|
const [quantity, setQuantity] = useState(1);
|
|
121
126
|
const [addingToCart, setAddingToCart] = useState(false);
|
|
122
127
|
const [addedMessage, setAddedMessage] = useState(false);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
128
|
+
|
|
129
|
+
const recommendations = product?.recommendations ?? null;
|
|
126
130
|
|
|
127
131
|
// Load product
|
|
128
132
|
useEffect(() => {
|
|
@@ -132,15 +136,12 @@ export default function ProductDetailPage() {
|
|
|
132
136
|
setError(null);
|
|
133
137
|
const client = getClient();
|
|
134
138
|
const p = await client.getProductBySlug(slug);
|
|
135
|
-
setProduct(p);
|
|
139
|
+
setProduct(p as ProductWithRecommendations);
|
|
136
140
|
|
|
137
141
|
// Auto-select first variant
|
|
138
142
|
if (p.variants && p.variants.length > 0) {
|
|
139
143
|
setSelectedVariant(p.variants[0]);
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
// Load recommendations in background
|
|
143
|
-
client.getProductRecommendations(p.id).then(setRecommendations).catch(() => {});
|
|
144
145
|
} catch {
|
|
145
146
|
setError(t('notFound'));
|
|
146
147
|
} finally {
|
|
@@ -341,8 +342,43 @@ export default function ProductDetailPage() {
|
|
|
341
342
|
size="lg"
|
|
342
343
|
/>
|
|
343
344
|
|
|
344
|
-
{/* Stock */}
|
|
345
|
-
|
|
345
|
+
{/* Stock / Digital badge */}
|
|
346
|
+
{product.isDownloadable ? (
|
|
347
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-950/30 dark:text-green-400">
|
|
348
|
+
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
349
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
350
|
+
</svg>
|
|
351
|
+
{t('instantDownload')}
|
|
352
|
+
</span>
|
|
353
|
+
) : (
|
|
354
|
+
<StockBadge inventory={inventory} lowStockThreshold={5} />
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Downloadable files info */}
|
|
358
|
+
{product.isDownloadable && product.downloads && product.downloads.length > 0 && (
|
|
359
|
+
<div className="bg-muted/50 rounded-lg border p-4">
|
|
360
|
+
<p className="text-foreground mb-2 text-sm font-medium">
|
|
361
|
+
{t('filesIncluded', { count: product.downloads.length })}
|
|
362
|
+
</p>
|
|
363
|
+
<ul className="space-y-1.5">
|
|
364
|
+
{product.downloads.map((file: DownloadFile) => (
|
|
365
|
+
<li key={file.id} className="text-muted-foreground flex items-center gap-2 text-sm">
|
|
366
|
+
<svg className="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
367
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
368
|
+
</svg>
|
|
369
|
+
<span className="truncate">{file.name}</span>
|
|
370
|
+
{file.size && (
|
|
371
|
+
<span className="flex-shrink-0 text-xs">
|
|
372
|
+
({file.size < 1024 * 1024
|
|
373
|
+
? `${(file.size / 1024).toFixed(0)} KB`
|
|
374
|
+
: `${(file.size / (1024 * 1024)).toFixed(1)} MB`})
|
|
375
|
+
</span>
|
|
376
|
+
)}
|
|
377
|
+
</li>
|
|
378
|
+
))}
|
|
379
|
+
</ul>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
346
382
|
|
|
347
383
|
{/* Variant Selector */}
|
|
348
384
|
{product.type === 'VARIABLE' && product.variants && product.variants.length > 0 && (
|
|
@@ -406,6 +442,13 @@ export default function ProductDetailPage() {
|
|
|
406
442
|
</button>
|
|
407
443
|
</div>
|
|
408
444
|
|
|
445
|
+
{/* Download after purchase note */}
|
|
446
|
+
{product.isDownloadable && (
|
|
447
|
+
<p className="text-muted-foreground text-sm">
|
|
448
|
+
{t('downloadAfterPurchase')}
|
|
449
|
+
</p>
|
|
450
|
+
)}
|
|
451
|
+
|
|
409
452
|
{/* Description */}
|
|
410
453
|
{description && (
|
|
411
454
|
<div className="border-border border-t pt-4">
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import Image from 'next/image';
|
|
5
|
-
import type { Order, OrderStatus } from 'brainerce';
|
|
5
|
+
import type { Order, OrderStatus, OrderDownloadLink } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
8
|
import { useTranslations } from '@/lib/translations';
|
|
8
9
|
import { cn } from '@/lib/utils';
|
|
9
10
|
|
|
@@ -187,6 +188,11 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
187
188
|
</div>
|
|
188
189
|
))}
|
|
189
190
|
|
|
191
|
+
{/* Downloads section */}
|
|
192
|
+
{order.hasDownloads && (
|
|
193
|
+
<OrderDownloads orderId={order.id} />
|
|
194
|
+
)}
|
|
195
|
+
|
|
190
196
|
<OrderFinancialSummary order={order} currency={currency} />
|
|
191
197
|
</div>
|
|
192
198
|
)}
|
|
@@ -194,6 +200,71 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
194
200
|
);
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
function OrderDownloads({ orderId }: { orderId: string }) {
|
|
204
|
+
const t = useTranslations('account');
|
|
205
|
+
const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
|
|
206
|
+
const [loading, setLoading] = useState(true);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
let cancelled = false;
|
|
210
|
+
async function fetch() {
|
|
211
|
+
try {
|
|
212
|
+
const client = getClient();
|
|
213
|
+
const links = await client.getOrderDownloads(orderId);
|
|
214
|
+
if (!cancelled) setDownloads(links);
|
|
215
|
+
} catch {
|
|
216
|
+
if (!cancelled) setDownloads([]);
|
|
217
|
+
} finally {
|
|
218
|
+
if (!cancelled) setLoading(false);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
fetch();
|
|
222
|
+
return () => { cancelled = true; };
|
|
223
|
+
}, [orderId]);
|
|
224
|
+
|
|
225
|
+
if (loading) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="border-border border-t pt-2">
|
|
228
|
+
<p className="text-muted-foreground animate-pulse text-xs">{t('downloads')}...</p>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!downloads || downloads.length === 0) return null;
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="border-border space-y-2 border-t pt-2">
|
|
237
|
+
<p className="text-foreground text-sm font-medium">{t('downloads')}</p>
|
|
238
|
+
{downloads.map((link, idx) => (
|
|
239
|
+
<div key={idx} className="flex items-center gap-3">
|
|
240
|
+
<div className="min-w-0 flex-1">
|
|
241
|
+
<p className="text-foreground truncate text-sm">{link.fileName}</p>
|
|
242
|
+
<p className="text-muted-foreground text-xs">
|
|
243
|
+
{link.productName}
|
|
244
|
+
{' · '}
|
|
245
|
+
{link.downloadLimit != null
|
|
246
|
+
? t('downloadsRemaining', { used: link.downloadsUsed, limit: link.downloadLimit })
|
|
247
|
+
: t('unlimitedDownloads')}
|
|
248
|
+
{' · '}
|
|
249
|
+
{link.expiresAt
|
|
250
|
+
? t('expiresAt', { date: new Date(link.expiresAt).toLocaleDateString() })
|
|
251
|
+
: t('noExpiry')}
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<a
|
|
255
|
+
href={link.downloadUrl}
|
|
256
|
+
target="_blank"
|
|
257
|
+
rel="noopener noreferrer"
|
|
258
|
+
className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1 text-xs font-medium hover:opacity-90"
|
|
259
|
+
>
|
|
260
|
+
{t('downloadFile')}
|
|
261
|
+
</a>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
197
268
|
function OrderFinancialSummary({ order, currency }: { order: Order; currency: string }) {
|
|
198
269
|
const tc = useTranslations('common');
|
|
199
270
|
const totalAmount = order.totalAmount || order.total || '0';
|
|
@@ -17,6 +17,7 @@ interface ProductCardProps {
|
|
|
17
17
|
|
|
18
18
|
export function ProductCard({ product, className }: ProductCardProps) {
|
|
19
19
|
const t = useTranslations('common');
|
|
20
|
+
const tp = useTranslations('productDetail');
|
|
20
21
|
const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
|
|
21
22
|
const mainImage = product.images?.[0];
|
|
22
23
|
const imageUrl = mainImage?.url || null;
|
|
@@ -61,6 +62,11 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
61
62
|
</span>
|
|
62
63
|
)}
|
|
63
64
|
<DiscountBadge discount={product.discount} />
|
|
65
|
+
{product.isDownloadable && (
|
|
66
|
+
<span className="bg-primary text-primary-foreground rounded px-2 py-1 text-xs font-bold">
|
|
67
|
+
{tp('digitalProduct')}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
64
70
|
</div>
|
|
65
71
|
</div>
|
|
66
72
|
|
|
@@ -13,7 +13,8 @@ interface RecommendationCardProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
16
|
-
const
|
|
16
|
+
const firstImage = item.images?.[0];
|
|
17
|
+
const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
|
|
17
18
|
const slug = item.slug || item.id;
|
|
18
19
|
const basePrice = parseFloat(item.basePrice);
|
|
19
20
|
const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
|