create-brainerce-store 1.83.0 → 1.85.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/README.md +130 -92
- package/dist/index.js +39 -17
- package/messages/en.json +3 -0
- package/messages/he.json +3 -0
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +10 -1
- package/templates/nextjs/base/AGENTS.md.ejs +229 -207
- package/templates/nextjs/base/AI-GUIDE.md +4 -0
- package/templates/nextjs/base/CLAUDE.md.ejs +240 -218
- package/templates/nextjs/base/scripts/connect.mjs +115 -18
- package/templates/nextjs/base/scripts/env-file.mjs +93 -0
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +109 -83
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +2 -1
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +3 -2
- package/templates/nextjs/base/src/app/layout.tsx.ejs +50 -21
- package/templates/nextjs/base/src/app/opengraph-image.tsx +5 -1
- package/templates/nextjs/base/src/app/reset-password/page.tsx +25 -138
- package/templates/nextjs/base/src/app/reset-password/reset-password-form.tsx +138 -0
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +174 -137
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +847 -761
- package/templates/nextjs/base/src/core/lib/render-mode.ts +49 -0
- package/templates/nextjs/base/src/core/lib/store-name.ts.ejs +56 -0
- package/templates/nextjs/base/src/core/lib/use-store-name.ts +18 -0
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +160 -160
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +2 -2
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +374 -345
- package/templates/nextjs/designs/atelier/globals.css +408 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +91 -96
- package/templates/nextjs/designs/atelier/messages-patch/he.json +91 -96
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +131 -128
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +2 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +222 -219
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +178 -151
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +61 -31
- package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +161 -155
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +232 -227
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +271 -268
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +558 -553
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +113 -110
- package/templates/nextjs/designs/atelier/ui/shared/no-photo-tile.tsx +73 -0
|
@@ -1,128 +1,131 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
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
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</
|
|
89
|
-
<
|
|
90
|
-
{formatPrice(parseFloat(p.
|
|
91
|
-
</
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
{formatPrice(
|
|
104
|
-
</
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import { NoPhotoTile } from '@/ui/shared/no-photo-tile';
|
|
6
|
+
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
7
|
+
import { formatPrice } from 'brainerce';
|
|
8
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
import { toAddToCartError, type AddToCartError } from '@/core/lib/add-to-cart-error';
|
|
11
|
+
import { cn } from '@/core/lib/utils';
|
|
12
|
+
import { IconBag } from '@/ui/shared/icons';
|
|
13
|
+
|
|
14
|
+
interface CartBundleOfferCardProps {
|
|
15
|
+
offer: CartBundleOfferType;
|
|
16
|
+
cartId: string;
|
|
17
|
+
onAdd: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bundle offer card on the cart page: offered products with original vs.
|
|
23
|
+
* discounted prices + add-bundle button; offers come from
|
|
24
|
+
* useCartPage().bundles.
|
|
25
|
+
*/
|
|
26
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
27
|
+
const t = useTranslations('cart');
|
|
28
|
+
const tp = useTranslations('productDetail');
|
|
29
|
+
const currency = useCurrency();
|
|
30
|
+
const [adding, setAdding] = useState(false);
|
|
31
|
+
const [addError, setAddError] = useState<AddToCartError | null>(null);
|
|
32
|
+
|
|
33
|
+
const offered = offer.offeredProducts;
|
|
34
|
+
|
|
35
|
+
const totalOriginal = parseFloat(offer.totalOriginalPrice);
|
|
36
|
+
const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
|
|
37
|
+
const discountLabel =
|
|
38
|
+
offer.discountType === 'PERCENTAGE'
|
|
39
|
+
? `${offer.discountValue}%`
|
|
40
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
41
|
+
|
|
42
|
+
async function handleAdd() {
|
|
43
|
+
if (adding) return;
|
|
44
|
+
setAddError(null);
|
|
45
|
+
try {
|
|
46
|
+
setAdding(true);
|
|
47
|
+
const { getClient } = await import('@/core/lib/brainerce');
|
|
48
|
+
const client = getClient();
|
|
49
|
+
// No variant selections in the default template — products with
|
|
50
|
+
// variants are best handled with a dedicated picker per offered item.
|
|
51
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
52
|
+
onAdd();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// The cart page is where a shopper is most likely to be AT the 50-line
|
|
55
|
+
// cap, and a bundle adds one line per offered product. Logging alone
|
|
56
|
+
// left the button spinning back to idle with nothing added.
|
|
57
|
+
setAddError(toAddToCartError(err));
|
|
58
|
+
console.error('Failed to add bundle:', err);
|
|
59
|
+
} finally {
|
|
60
|
+
setAdding(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<article className={cn('card p-5', className)}>
|
|
66
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
67
|
+
<h4 className="font-sans text-base font-semibold">{offer.name}</h4>
|
|
68
|
+
<span className="badge-primary">-{discountLabel}</span>
|
|
69
|
+
</div>
|
|
70
|
+
{offer.description && (
|
|
71
|
+
<p className="mt-1 text-sm text-muted-foreground">{offer.description}</p>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
<ul className="mt-4 space-y-2.5">
|
|
75
|
+
{offered.map((p) => {
|
|
76
|
+
const firstImage = p.images?.[0];
|
|
77
|
+
const imageUrl = firstImage?.url ?? null;
|
|
78
|
+
return (
|
|
79
|
+
<li key={p.id} className="flex items-center gap-3">
|
|
80
|
+
{/* `relative` + fixed box are layout-critical for next/image fill */}
|
|
81
|
+
<span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-secondary">
|
|
82
|
+
{imageUrl ? (
|
|
83
|
+
<Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
|
|
84
|
+
) : (
|
|
85
|
+
<NoPhotoTile name={p.name} size="sm" />
|
|
86
|
+
)}
|
|
87
|
+
</span>
|
|
88
|
+
<span className="min-w-0 flex-1 text-sm font-medium text-foreground">{p.name}</span>
|
|
89
|
+
<s className="text-xs text-muted-foreground">
|
|
90
|
+
{formatPrice(parseFloat(p.originalPrice), { currency }) as string}
|
|
91
|
+
</s>
|
|
92
|
+
<span className="text-sm font-semibold text-foreground">
|
|
93
|
+
{formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
|
|
94
|
+
</span>
|
|
95
|
+
</li>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</ul>
|
|
99
|
+
|
|
100
|
+
<div className="mt-4 flex flex-wrap items-center gap-3 border-t pt-4">
|
|
101
|
+
<span className="inline-flex items-baseline gap-2">
|
|
102
|
+
<s className="text-sm text-muted-foreground">
|
|
103
|
+
{formatPrice(totalOriginal, { currency }) as string}
|
|
104
|
+
</s>
|
|
105
|
+
<span className="text-lg font-bold text-foreground">
|
|
106
|
+
{formatPrice(totalDiscounted, { currency }) as string}
|
|
107
|
+
</span>
|
|
108
|
+
</span>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={handleAdd}
|
|
112
|
+
disabled={adding}
|
|
113
|
+
className="btn-primary btn-sm ms-auto"
|
|
114
|
+
>
|
|
115
|
+
<IconBag size={16} />
|
|
116
|
+
{adding ? t('addingBundle') : t('addBundleItem')}
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/*
|
|
121
|
+
Why the bundle was refused. Without this the button resets and
|
|
122
|
+
nothing is added, which reads as a broken button.
|
|
123
|
+
*/}
|
|
124
|
+
{addError && (
|
|
125
|
+
<p role="alert" className="text-destructive mt-2 text-xs">
|
|
126
|
+
{addError === 'CART_FULL' ? tp('cartFull') : tp('addToCartFailed')}
|
|
127
|
+
</p>
|
|
128
|
+
)}
|
|
129
|
+
</article>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -12,6 +12,7 @@ import { cn } from '@/core/lib/utils';
|
|
|
12
12
|
import { getErrorCode } from '@/core/lib/add-to-cart-error';
|
|
13
13
|
import { OrderCustomizations } from '@/components/account/order-customizations';
|
|
14
14
|
import { IconMinus, IconPlus, IconTrash } from '@/ui/shared/icons';
|
|
15
|
+
import { NoPhotoTile } from '@/ui/shared/no-photo-tile';
|
|
15
16
|
|
|
16
17
|
interface CartItemProps {
|
|
17
18
|
item: CartItemType;
|
|
@@ -122,7 +123,7 @@ export function CartItem({ item, onUpdate, className }: CartItemProps) {
|
|
|
122
123
|
{imageUrl ? (
|
|
123
124
|
<Image src={imageUrl} alt={productName} fill sizes="96px" className="object-cover" />
|
|
124
125
|
) : (
|
|
125
|
-
<
|
|
126
|
+
<NoPhotoTile name={productName} size="sm" />
|
|
126
127
|
)}
|
|
127
128
|
</div>
|
|
128
129
|
|