create-brainerce-store 1.84.0 → 1.86.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 (52) hide show
  1. package/README.md +130 -92
  2. package/dist/index.js +39 -17
  3. package/messages/en.json +9 -2
  4. package/messages/he.json +9 -2
  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/category/[slug]/page.tsx +2 -0
  15. package/templates/nextjs/base/src/app/checkout/page.tsx +1325 -1302
  16. package/templates/nextjs/base/src/app/forgot-password/page.tsx +3 -2
  17. package/templates/nextjs/base/src/app/icon.svg +11 -0
  18. package/templates/nextjs/base/src/app/layout.tsx.ejs +50 -21
  19. package/templates/nextjs/base/src/app/opengraph-image.tsx +5 -1
  20. package/templates/nextjs/base/src/app/reset-password/page.tsx +25 -138
  21. package/templates/nextjs/base/src/app/reset-password/reset-password-form.tsx +138 -0
  22. package/templates/nextjs/base/src/components/account/order-payment-block.tsx +17 -2
  23. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +174 -137
  24. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +130 -12
  25. package/templates/nextjs/base/src/core/lib/store-name.ts.ejs +56 -0
  26. package/templates/nextjs/base/src/core/lib/tracking.ts +25 -14
  27. package/templates/nextjs/base/src/core/lib/use-store-name.ts +18 -0
  28. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  29. package/templates/nextjs/base/src/ui/layout/header-search.tsx +2 -2
  30. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +160 -160
  31. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +2 -2
  32. package/templates/nextjs/base/src/ui/product/price-display.tsx +85 -62
  33. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +539 -537
  34. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +374 -345
  35. package/templates/nextjs/designs/atelier/globals.css +408 -402
  36. package/templates/nextjs/designs/atelier/messages-patch/en.json +91 -96
  37. package/templates/nextjs/designs/atelier/messages-patch/he.json +91 -96
  38. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +131 -128
  39. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +2 -1
  40. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +222 -219
  41. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +178 -151
  42. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +61 -31
  43. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +2 -2
  44. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +161 -155
  45. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +232 -227
  46. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +90 -69
  47. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +271 -268
  48. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +560 -553
  49. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +113 -110
  50. package/templates/nextjs/designs/atelier/ui/shared/no-photo-tile.tsx +73 -0
  51. package/templates/nextjs/ui-canvas/product/price-display.tsx +73 -52
  52. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +428 -426
@@ -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