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