create-brainerce-store 1.13.0 → 1.14.1
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/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +4 -0
- package/templates/nextjs/base/package.json.ejs +3 -2
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +74 -0
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +481 -485
- package/templates/nextjs/base/src/app/products/page.tsx +5 -5
- package/templates/nextjs/base/src/app/robots.ts +14 -14
- package/templates/nextjs/base/src/app/sitemap.ts +25 -25
- package/templates/nextjs/base/src/components/account/order-history.tsx +3 -3
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +1 -3
- package/templates/nextjs/base/src/components/layout/header.tsx +1 -1
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +30 -11
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +39 -39
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +1 -0
|
@@ -69,19 +69,19 @@ function ChevronDown({ className }: { className?: string }) {
|
|
|
69
69
|
|
|
70
70
|
/** Recursive dropdown items for nested categories */
|
|
71
71
|
function CategoryDropdownItems({
|
|
72
|
-
|
|
72
|
+
items,
|
|
73
73
|
depth,
|
|
74
74
|
selectedId,
|
|
75
75
|
onSelect,
|
|
76
76
|
}: {
|
|
77
|
-
|
|
77
|
+
items: CategoryNode[];
|
|
78
78
|
depth: number;
|
|
79
79
|
selectedId: string;
|
|
80
80
|
onSelect: (id: string) => void;
|
|
81
81
|
}) {
|
|
82
82
|
return (
|
|
83
83
|
<>
|
|
84
|
-
{
|
|
84
|
+
{items.map((child) => (
|
|
85
85
|
<div key={child.id}>
|
|
86
86
|
<button
|
|
87
87
|
onClick={() => onSelect(child.id)}
|
|
@@ -95,7 +95,7 @@ function CategoryDropdownItems({
|
|
|
95
95
|
</button>
|
|
96
96
|
{child.children.length > 0 && (
|
|
97
97
|
<CategoryDropdownItems
|
|
98
|
-
|
|
98
|
+
items={child.children}
|
|
99
99
|
depth={depth + 1}
|
|
100
100
|
selectedId={selectedId}
|
|
101
101
|
onSelect={onSelect}
|
|
@@ -204,7 +204,7 @@ function CategoryChip({
|
|
|
204
204
|
{/* Recursive children */}
|
|
205
205
|
<div onClick={() => setOpen(false)}>
|
|
206
206
|
<CategoryDropdownItems
|
|
207
|
-
|
|
207
|
+
items={category.children}
|
|
208
208
|
depth={0}
|
|
209
209
|
selectedId={selectedId}
|
|
210
210
|
onSelect={onSelect}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { MetadataRoute } from 'next';
|
|
2
|
-
|
|
3
|
-
export default function robots(): MetadataRoute.Robots {
|
|
4
|
-
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
rules: {
|
|
8
|
-
userAgent: '*',
|
|
9
|
-
allow: '/',
|
|
10
|
-
disallow: ['/api/', '/auth/', '/checkout/', '/account/'],
|
|
11
|
-
},
|
|
12
|
-
sitemap: `${baseUrl}/sitemap.xml`,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
|
|
3
|
+
export default function robots(): MetadataRoute.Robots {
|
|
4
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
rules: {
|
|
8
|
+
userAgent: '*',
|
|
9
|
+
allow: '/',
|
|
10
|
+
disallow: ['/api/', '/auth/', '/checkout/', '/account/'],
|
|
11
|
+
},
|
|
12
|
+
sitemap: `${baseUrl}/sitemap.xml`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { MetadataRoute } from 'next';
|
|
2
|
-
import { getServerClient } from '@/lib/brainerce';
|
|
3
|
-
|
|
4
|
-
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
5
|
-
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
6
|
-
|
|
7
|
-
const staticPages: MetadataRoute.Sitemap = [
|
|
8
|
-
{ url: baseUrl, lastModified: new Date(), priority: 1 },
|
|
9
|
-
{ url: `${baseUrl}/products`, lastModified: new Date(), priority: 0.9 },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const client = getServerClient();
|
|
14
|
-
const { data: products } = await client.getProducts({ limit: 1000 });
|
|
15
|
-
const productPages: MetadataRoute.Sitemap = products.map((product) => ({
|
|
16
|
-
url: `${baseUrl}/products/${product.slug}`,
|
|
17
|
-
lastModified: product.updatedAt ? new Date(product.updatedAt) : new Date(),
|
|
18
|
-
priority: 0.8,
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
return [...staticPages, ...productPages];
|
|
22
|
-
} catch {
|
|
23
|
-
return staticPages;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
import { getServerClient } from '@/lib/brainerce';
|
|
3
|
+
|
|
4
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
5
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
6
|
+
|
|
7
|
+
const staticPages: MetadataRoute.Sitemap = [
|
|
8
|
+
{ url: baseUrl, lastModified: new Date(), priority: 1 },
|
|
9
|
+
{ url: `${baseUrl}/products`, lastModified: new Date(), priority: 0.9 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const client = getServerClient();
|
|
14
|
+
const { data: products } = await client.getProducts({ limit: 1000 });
|
|
15
|
+
const productPages: MetadataRoute.Sitemap = products.map((product) => ({
|
|
16
|
+
url: `${baseUrl}/products/${product.slug}`,
|
|
17
|
+
lastModified: product.updatedAt ? new Date(product.updatedAt) : new Date(),
|
|
18
|
+
priority: 0.8,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
return [...staticPages, ...productPages];
|
|
22
|
+
} catch {
|
|
23
|
+
return staticPages;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -77,7 +77,7 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
77
77
|
const t = useTranslations('account');
|
|
78
78
|
const tc = useTranslations('common');
|
|
79
79
|
const [expanded, setExpanded] = useState(false);
|
|
80
|
-
const statusConfig = STATUS_CONFIG[order.status] || STATUS_CONFIG.pending;
|
|
80
|
+
const statusConfig = STATUS_CONFIG[order.status?.toLowerCase() as OrderStatus] || STATUS_CONFIG.pending;
|
|
81
81
|
const currency = order.currency || 'USD';
|
|
82
82
|
const totalAmount = order.totalAmount || order.total || '0';
|
|
83
83
|
|
|
@@ -243,11 +243,11 @@ function OrderDownloads({ orderId }: { orderId: string }) {
|
|
|
243
243
|
{link.productName}
|
|
244
244
|
{' · '}
|
|
245
245
|
{link.downloadLimit != null
|
|
246
|
-
?
|
|
246
|
+
? `${link.downloadsUsed}/${link.downloadLimit} ${t('downloadsRemaining')}`
|
|
247
247
|
: t('unlimitedDownloads')}
|
|
248
248
|
{' · '}
|
|
249
249
|
{link.expiresAt
|
|
250
|
-
? t('expiresAt'
|
|
250
|
+
? `${t('expiresAt')} ${new Date(link.expiresAt).toLocaleDateString()}`
|
|
251
251
|
: t('noExpiry')}
|
|
252
252
|
</p>
|
|
253
253
|
</div>
|
|
@@ -243,9 +243,7 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
243
243
|
},
|
|
244
244
|
};
|
|
245
245
|
|
|
246
|
-
console.info(
|
|
247
|
-
`Payment SDK: ${method}({ environment: "${config.environment}", version: ${config.version} })`
|
|
248
|
-
);
|
|
246
|
+
console.info(`Payment SDK: calling ${method}()`);
|
|
249
247
|
global[method](config);
|
|
250
248
|
sdkInitDone = true;
|
|
251
249
|
}
|
|
@@ -81,7 +81,7 @@ export function Header() {
|
|
|
81
81
|
<div className="flex h-16 items-center justify-between gap-4">
|
|
82
82
|
{/* Logo / Store Name */}
|
|
83
83
|
<Link href="/" className="text-foreground flex-shrink-0 text-xl font-bold">
|
|
84
|
-
{storeInfo?.name || tc('store')}
|
|
84
|
+
{storeInfo?.name || process.env.NEXT_PUBLIC_STORE_NAME || tc('store')}
|
|
85
85
|
</Link>
|
|
86
86
|
|
|
87
87
|
{/* Desktop Navigation */}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { getStockStatus } from 'brainerce';
|
|
4
3
|
import type { InventoryInfo } from 'brainerce';
|
|
5
4
|
import { cn } from '@/lib/utils';
|
|
6
5
|
|
|
@@ -11,24 +10,44 @@ interface StockBadgeProps {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function StockBadge({ inventory, lowStockThreshold = 5, className }: StockBadgeProps) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const colorClasses =
|
|
17
|
-
status === 'Out of Stock' || status === 'Unavailable'
|
|
18
|
-
? 'bg-red-100 text-red-800'
|
|
19
|
-
: status === 'Low Stock'
|
|
20
|
-
? 'bg-yellow-100 text-yellow-800'
|
|
21
|
-
: 'bg-green-100 text-green-800';
|
|
13
|
+
const label = getStockLabel(inventory, lowStockThreshold);
|
|
14
|
+
const color = getStockColor(inventory, lowStockThreshold);
|
|
22
15
|
|
|
23
16
|
return (
|
|
24
17
|
<span
|
|
25
18
|
className={cn(
|
|
26
19
|
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
27
|
-
|
|
20
|
+
color,
|
|
28
21
|
className
|
|
29
22
|
)}
|
|
30
23
|
>
|
|
31
|
-
{
|
|
24
|
+
{label}
|
|
32
25
|
</span>
|
|
33
26
|
);
|
|
34
27
|
}
|
|
28
|
+
|
|
29
|
+
function getStockLabel(inventory: InventoryInfo | null | undefined, lowStockThreshold: number): string {
|
|
30
|
+
if (!inventory) return 'Out of Stock';
|
|
31
|
+
|
|
32
|
+
const { trackingMode, inStock, available } = inventory;
|
|
33
|
+
|
|
34
|
+
if (trackingMode === 'DISABLED') return 'Unavailable';
|
|
35
|
+
if (!inStock) return 'Out of Stock';
|
|
36
|
+
if (trackingMode === 'UNLIMITED') return 'In Stock';
|
|
37
|
+
|
|
38
|
+
// TRACKED — show actual quantity
|
|
39
|
+
if (available <= lowStockThreshold) {
|
|
40
|
+
return `Only ${available} left`;
|
|
41
|
+
}
|
|
42
|
+
return `${available} in stock`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStockColor(inventory: InventoryInfo | null | undefined, lowStockThreshold: number): string {
|
|
46
|
+
if (!inventory) return 'bg-red-100 text-red-800';
|
|
47
|
+
|
|
48
|
+
const { trackingMode, inStock, available } = inventory;
|
|
49
|
+
|
|
50
|
+
if (trackingMode === 'DISABLED' || !inStock) return 'bg-red-100 text-red-800';
|
|
51
|
+
if (trackingMode === 'TRACKED' && available <= lowStockThreshold) return 'bg-yellow-100 text-yellow-800';
|
|
52
|
+
return 'bg-green-100 text-green-800';
|
|
53
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import type { Product } from 'brainerce';
|
|
2
|
-
import { getProductPriceInfo } from 'brainerce';
|
|
3
|
-
|
|
4
|
-
interface ProductJsonLdProps {
|
|
5
|
-
product: Product;
|
|
6
|
-
url: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function ProductJsonLd({ product, url }: ProductJsonLdProps) {
|
|
10
|
-
const priceInfo = getProductPriceInfo(product);
|
|
11
|
-
const imageUrl = product.images?.[0]?.url;
|
|
12
|
-
|
|
13
|
-
const jsonLd = {
|
|
14
|
-
'@context': 'https://schema.org',
|
|
15
|
-
'@type': 'Product',
|
|
16
|
-
name: product.name,
|
|
17
|
-
description: product.description || product.name,
|
|
18
|
-
image: imageUrl,
|
|
19
|
-
url,
|
|
20
|
-
sku: product.sku || product.id,
|
|
21
|
-
offers: {
|
|
22
|
-
'@type': 'Offer',
|
|
23
|
-
price: priceInfo.price,
|
|
24
|
-
priceCurrency: 'ILS',
|
|
25
|
-
availability:
|
|
26
|
-
product.inventory?.canPurchase !== false
|
|
27
|
-
? 'https://schema.org/InStock'
|
|
28
|
-
: 'https://schema.org/OutOfStock',
|
|
29
|
-
url,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<script
|
|
35
|
-
type="application/ld+json"
|
|
36
|
-
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
1
|
+
import type { Product } from 'brainerce';
|
|
2
|
+
import { getProductPriceInfo } from 'brainerce';
|
|
3
|
+
|
|
4
|
+
interface ProductJsonLdProps {
|
|
5
|
+
product: Product;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ProductJsonLd({ product, url }: ProductJsonLdProps) {
|
|
10
|
+
const priceInfo = getProductPriceInfo(product);
|
|
11
|
+
const imageUrl = product.images?.[0]?.url;
|
|
12
|
+
|
|
13
|
+
const jsonLd = {
|
|
14
|
+
'@context': 'https://schema.org',
|
|
15
|
+
'@type': 'Product',
|
|
16
|
+
name: product.name,
|
|
17
|
+
description: product.description || product.name,
|
|
18
|
+
image: imageUrl,
|
|
19
|
+
url,
|
|
20
|
+
sku: product.sku || product.id,
|
|
21
|
+
offers: {
|
|
22
|
+
'@type': 'Offer',
|
|
23
|
+
price: priceInfo.price,
|
|
24
|
+
priceCurrency: 'ILS',
|
|
25
|
+
availability:
|
|
26
|
+
product.inventory?.canPurchase !== false
|
|
27
|
+
? 'https://schema.org/InStock'
|
|
28
|
+
: 'https://schema.org/OutOfStock',
|
|
29
|
+
url,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<script
|
|
35
|
+
type="application/ld+json"
|
|
36
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|