create-brainerce-store 1.9.1 → 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 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.9.0",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.9.1",
3
+ "version": "1.11.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -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<Product | null>(null);
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
- const [recommendations, setRecommendations] = useState<ProductRecommendationsResponse | null>(
124
- null
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
- <StockBadge inventory={inventory} lowStockThreshold={5} />
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 imageUrl = item.images?.[0]?.url || null;
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;