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.
- package/dist/index.js +62 -5
- package/messages/en.json +258 -0
- package/messages/he.json +258 -0
- package/package.json +3 -2
- package/templates/nextjs/base/src/app/account/page.tsx +108 -105
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -88
- package/templates/nextjs/base/src/app/cart/page.tsx +110 -109
- package/templates/nextjs/base/src/app/checkout/page.tsx +46 -43
- package/templates/nextjs/base/src/app/layout.tsx.ejs +8 -5
- package/templates/nextjs/base/src/app/login/page.tsx +58 -56
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +18 -23
- package/templates/nextjs/base/src/app/page.tsx +98 -95
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +16 -12
- package/templates/nextjs/base/src/app/products/page.tsx +246 -243
- package/templates/nextjs/base/src/app/register/page.tsx +68 -66
- package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -291
- package/templates/nextjs/base/src/components/account/order-history.tsx +198 -184
- package/templates/nextjs/base/src/components/account/profile-section.tsx +75 -73
- package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -92
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -134
- package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -177
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -150
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -67
- package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -131
- package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -100
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +28 -25
- package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +6 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +133 -103
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +15 -11
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -111
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +7 -4
- package/templates/nextjs/base/src/components/layout/footer.tsx +38 -35
- package/templates/nextjs/base/src/components/layout/header.tsx +332 -329
- package/templates/nextjs/base/src/components/products/product-card.tsx +3 -1
- package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -33
- package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -30
- package/templates/nextjs/base/src/i18n.ts.ejs +5 -0
- 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
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const [
|
|
15
|
-
const [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
</
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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' ? '
|
|
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('
|
|
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 || '
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
384
|
+
{t('addingToCart')}
|
|
381
385
|
</span>
|
|
382
386
|
) : addedMessage ? (
|
|
383
|
-
'
|
|
387
|
+
t('addedToCart')
|
|
384
388
|
) : !canPurchase ? (
|
|
385
|
-
'
|
|
389
|
+
t('outOfStock')
|
|
386
390
|
) : (
|
|
387
|
-
'
|
|
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">
|
|
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">
|
|
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) => (
|