create-brainerce-store 1.3.2 → 1.4.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.
Files changed (38) hide show
  1. package/dist/index.js +62 -5
  2. package/messages/en.json +258 -0
  3. package/messages/he.json +258 -0
  4. package/package.json +3 -2
  5. package/templates/nextjs/base/src/app/account/page.tsx +108 -105
  6. package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -88
  7. package/templates/nextjs/base/src/app/cart/page.tsx +110 -109
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +46 -43
  9. package/templates/nextjs/base/src/app/layout.tsx.ejs +8 -5
  10. package/templates/nextjs/base/src/app/login/page.tsx +58 -56
  11. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +18 -23
  12. package/templates/nextjs/base/src/app/page.tsx +98 -95
  13. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +16 -12
  14. package/templates/nextjs/base/src/app/products/page.tsx +246 -243
  15. package/templates/nextjs/base/src/app/register/page.tsx +68 -66
  16. package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -291
  17. package/templates/nextjs/base/src/components/account/order-history.tsx +198 -184
  18. package/templates/nextjs/base/src/components/account/profile-section.tsx +75 -73
  19. package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -92
  20. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -134
  21. package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -177
  22. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -150
  23. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -67
  24. package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -131
  25. package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -100
  26. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +28 -25
  27. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +6 -4
  28. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +133 -103
  29. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +15 -11
  30. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -111
  31. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +7 -4
  32. package/templates/nextjs/base/src/components/layout/footer.tsx +38 -35
  33. package/templates/nextjs/base/src/components/layout/header.tsx +332 -329
  34. package/templates/nextjs/base/src/components/products/product-card.tsx +3 -1
  35. package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -33
  36. package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -30
  37. package/templates/nextjs/base/src/i18n.ts.ejs +5 -0
  38. package/templates/nextjs/base/src/lib/translations.ts +11 -0
@@ -1,95 +1,98 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from 'react';
4
- import Link from 'next/link';
5
- import type { Product, DiscountBanner } from 'brainerce';
6
- import { getClient } from '@/lib/brainerce';
7
- import { useStoreInfo } from '@/providers/store-provider';
8
- import { ProductGrid } from '@/components/products/product-grid';
9
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
-
11
- export default function HomePage() {
12
- const { storeInfo, loading: storeLoading } = useStoreInfo();
13
- const [products, setProducts] = useState<Product[]>([]);
14
- const [banners, setBanners] = useState<DiscountBanner[]>([]);
15
- const [loading, setLoading] = useState(true);
16
-
17
- useEffect(() => {
18
- async function load() {
19
- try {
20
- const client = getClient();
21
- const [productsRes, bannersRes] = await Promise.allSettled([
22
- client.getProducts({ limit: 8, sortBy: 'createdAt', sortOrder: 'desc' }),
23
- client.getDiscountBanners(),
24
- ]);
25
-
26
- if (productsRes.status === 'fulfilled') {
27
- setProducts(productsRes.value.data);
28
- }
29
- if (bannersRes.status === 'fulfilled') {
30
- setBanners(bannersRes.value);
31
- }
32
- } catch (err) {
33
- console.error('Failed to load home page data:', err);
34
- } finally {
35
- setLoading(false);
36
- }
37
- }
38
-
39
- load();
40
- }, []);
41
-
42
- if (storeLoading || loading) {
43
- return (
44
- <div className="flex min-h-[60vh] items-center justify-center">
45
- <LoadingSpinner size="lg" />
46
- </div>
47
- );
48
- }
49
-
50
- return (
51
- <div>
52
- {/* Discount Banners */}
53
- {banners.length > 0 && (
54
- <div className="bg-primary text-primary-foreground">
55
- <div className="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
56
- <div className="flex items-center justify-center gap-4 overflow-x-auto text-sm font-medium">
57
- {banners.map((banner) => (
58
- <span key={banner.ruleId}>{banner.text}</span>
59
- ))}
60
- </div>
61
- </div>
62
- </div>
63
- )}
64
-
65
- {/* Hero Section */}
66
- <section className="bg-muted">
67
- <div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
68
- <h1 className="text-foreground text-4xl font-bold tracking-tight sm:text-5xl">
69
- Welcome to {storeInfo?.name || 'Our Store'}
70
- </h1>
71
- <p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-lg">
72
- Discover our curated collection of products crafted with care.
73
- </p>
74
- <Link
75
- href="/products"
76
- className="bg-primary text-primary-foreground mt-8 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
77
- >
78
- Shop Now
79
- </Link>
80
- </div>
81
- </section>
82
-
83
- {/* Featured Products */}
84
- <section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
85
- <div className="mb-8 flex items-center justify-between">
86
- <h2 className="text-foreground text-2xl font-bold">Featured Products</h2>
87
- <Link href="/products" className="text-primary text-sm font-medium hover:underline">
88
- View all
89
- </Link>
90
- </div>
91
- <ProductGrid products={products} />
92
- </section>
93
- </div>
94
- );
95
- }
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import type { Product, DiscountBanner } from 'brainerce';
6
+ import { getClient } from '@/lib/brainerce';
7
+ import { useStoreInfo } from '@/providers/store-provider';
8
+ import { ProductGrid } from '@/components/products/product-grid';
9
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
+ import { useTranslations } from '@/lib/translations';
11
+
12
+ export default function HomePage() {
13
+ const { storeInfo, loading: storeLoading } = useStoreInfo();
14
+ const [products, setProducts] = useState<Product[]>([]);
15
+ const [banners, setBanners] = useState<DiscountBanner[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const t = useTranslations('home');
18
+ const tc = useTranslations('common');
19
+
20
+ useEffect(() => {
21
+ async function load() {
22
+ try {
23
+ const client = getClient();
24
+ const [productsRes, bannersRes] = await Promise.allSettled([
25
+ client.getProducts({ limit: 8, sortBy: 'createdAt', sortOrder: 'desc' }),
26
+ client.getDiscountBanners(),
27
+ ]);
28
+
29
+ if (productsRes.status === 'fulfilled') {
30
+ setProducts(productsRes.value.data);
31
+ }
32
+ if (bannersRes.status === 'fulfilled') {
33
+ setBanners(bannersRes.value);
34
+ }
35
+ } catch (err) {
36
+ console.error('Failed to load home page data:', err);
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ }
41
+
42
+ load();
43
+ }, []);
44
+
45
+ if (storeLoading || loading) {
46
+ return (
47
+ <div className="flex min-h-[60vh] items-center justify-center">
48
+ <LoadingSpinner size="lg" />
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div>
55
+ {/* Discount Banners */}
56
+ {banners.length > 0 && (
57
+ <div className="bg-primary text-primary-foreground">
58
+ <div className="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
59
+ <div className="flex items-center justify-center gap-4 overflow-x-auto text-sm font-medium">
60
+ {banners.map((banner) => (
61
+ <span key={banner.ruleId}>{banner.text}</span>
62
+ ))}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ )}
67
+
68
+ {/* Hero Section */}
69
+ <section className="bg-muted">
70
+ <div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
71
+ <h1 className="text-foreground text-4xl font-bold tracking-tight sm:text-5xl">
72
+ {t('welcomeTo')} {storeInfo?.name || tc('store')}
73
+ </h1>
74
+ <p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-lg">
75
+ {t('heroSubtitle')}
76
+ </p>
77
+ <Link
78
+ href="/products"
79
+ className="bg-primary text-primary-foreground mt-8 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
80
+ >
81
+ {tc('shopNow')}
82
+ </Link>
83
+ </div>
84
+ </section>
85
+
86
+ {/* Featured Products */}
87
+ <section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
88
+ <div className="mb-8 flex items-center justify-between">
89
+ <h2 className="text-foreground text-2xl font-bold">{t('featuredProducts')}</h2>
90
+ <Link href="/products" className="text-primary text-sm font-medium hover:underline">
91
+ {tc('viewAll')}
92
+ </Link>
93
+ </div>
94
+ <ProductGrid products={products} />
95
+ </section>
96
+ </div>
97
+ );
98
+ }
@@ -12,10 +12,12 @@ import { PriceDisplay } from '@/components/shared/price-display';
12
12
  import { LoadingSpinner } from '@/components/shared/loading-spinner';
13
13
  import { VariantSelector } from '@/components/products/variant-selector';
14
14
  import { StockBadge } from '@/components/products/stock-badge';
15
+ import { useTranslations } from '@/lib/translations';
15
16
  import { cn } from '@/lib/utils';
16
17
 
17
18
  /** Render a metafield value based on its type */
18
19
  function MetafieldValue({ field }: { field: ProductMetafield }) {
20
+ const tc = useTranslations('common');
19
21
  switch (field.type) {
20
22
  case 'IMAGE': {
21
23
  if (!field.value) return <span className="text-muted-foreground">-</span>;
@@ -77,7 +79,7 @@ function MetafieldValue({ field }: { field: ProductMetafield }) {
77
79
  <span className="text-muted-foreground">-</span>
78
80
  );
79
81
  case 'BOOLEAN':
80
- return <span>{field.value === 'true' ? 'Yes' : 'No'}</span>;
82
+ return <span>{field.value === 'true' ? tc('yes') : tc('no')}</span>;
81
83
  case 'DATE':
82
84
  case 'DATETIME': {
83
85
  if (!field.value) return <span className="text-muted-foreground">-</span>;
@@ -101,6 +103,8 @@ export default function ProductDetailPage() {
101
103
  const params = useParams();
102
104
  const slug = params.slug as string;
103
105
  const { refreshCart } = useCart();
106
+ const t = useTranslations('productDetail');
107
+ const tc = useTranslations('common');
104
108
  const [product, setProduct] = useState<Product | null>(null);
105
109
  const [loading, setLoading] = useState(true);
106
110
  const [error, setError] = useState<string | null>(null);
@@ -125,7 +129,7 @@ export default function ProductDetailPage() {
125
129
  setSelectedVariant(p.variants[0]);
126
130
  }
127
131
  } catch {
128
- setError('Product not found.');
132
+ setError(t('notFound'));
129
133
  } finally {
130
134
  setLoading(false);
131
135
  }
@@ -232,9 +236,9 @@ export default function ProductDetailPage() {
232
236
  if (error || !product) {
233
237
  return (
234
238
  <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
235
- <h1 className="text-foreground text-2xl font-bold">{error || 'Product not found'}</h1>
239
+ <h1 className="text-foreground text-2xl font-bold">{error || t('notFound')}</h1>
236
240
  <Link href="/products" className="text-primary mt-4 inline-block hover:underline">
237
- Back to products
241
+ {t('backToProducts')}
238
242
  </Link>
239
243
  </div>
240
244
  );
@@ -343,7 +347,7 @@ export default function ProductDetailPage() {
343
347
  type="button"
344
348
  onClick={() => setQuantity((q) => Math.max(1, q - 1))}
345
349
  className="text-foreground hover:bg-muted px-3 py-2 transition-colors"
346
- aria-label="Decrease quantity"
350
+ aria-label={t('decreaseQuantity')}
347
351
  >
348
352
  -
349
353
  </button>
@@ -354,7 +358,7 @@ export default function ProductDetailPage() {
354
358
  type="button"
355
359
  onClick={() => setQuantity((q) => q + 1)}
356
360
  className="text-foreground hover:bg-muted px-3 py-2 transition-colors"
357
- aria-label="Increase quantity"
361
+ aria-label={t('increaseQuantity')}
358
362
  >
359
363
  +
360
364
  </button>
@@ -377,14 +381,14 @@ export default function ProductDetailPage() {
377
381
  size="sm"
378
382
  className="border-primary-foreground/30 border-t-primary-foreground"
379
383
  />
380
- Adding...
384
+ {t('addingToCart')}
381
385
  </span>
382
386
  ) : addedMessage ? (
383
- 'Added to Cart!'
387
+ t('addedToCart')
384
388
  ) : !canPurchase ? (
385
- 'Out of Stock'
389
+ t('outOfStock')
386
390
  ) : (
387
- 'Add to Cart'
391
+ t('addToCart')
388
392
  )}
389
393
  </button>
390
394
  </div>
@@ -392,7 +396,7 @@ export default function ProductDetailPage() {
392
396
  {/* Description */}
393
397
  {description && (
394
398
  <div className="border-border border-t pt-4">
395
- <h2 className="text-foreground mb-3 text-lg font-semibold">Description</h2>
399
+ <h2 className="text-foreground mb-3 text-lg font-semibold">{t('description')}</h2>
396
400
  {'html' in description ? (
397
401
  <div
398
402
  className="prose prose-sm text-muted-foreground max-w-none"
@@ -407,7 +411,7 @@ export default function ProductDetailPage() {
407
411
  {/* Metafields / Specifications */}
408
412
  {product.metafields && product.metafields.length > 0 && (
409
413
  <div className="border-border border-t pt-4">
410
- <h2 className="text-foreground mb-3 text-lg font-semibold">Specifications</h2>
414
+ <h2 className="text-foreground mb-3 text-lg font-semibold">{t('specifications')}</h2>
411
415
  <table className="w-full text-sm">
412
416
  <tbody>
413
417
  {product.metafields.map((field) => (