create-brainerce-store 1.67.0 → 1.71.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 (40) hide show
  1. package/dist/index.js +22 -2
  2. package/messages/en.json +52 -2
  3. package/messages/he.json +52 -2
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.env.local.ejs +7 -0
  6. package/templates/nextjs/base/AGENTS.md.ejs +7 -0
  7. package/templates/nextjs/base/CLAUDE.md.ejs +7 -0
  8. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +8 -2
  9. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +16 -7
  10. package/templates/nextjs/base/src/app/checkout/page.tsx +1018 -1017
  11. package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
  12. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +8 -2
  13. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +17 -7
  14. package/templates/nextjs/base/src/app/register/page.tsx +67 -64
  15. package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
  16. package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
  17. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
  18. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
  19. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
  20. package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
  21. package/templates/nextjs/base/src/core/lib/auth.ts +162 -154
  22. package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
  23. package/templates/nextjs/base/src/core/lib/site-url.ts +42 -9
  24. package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
  25. package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
  26. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
  27. package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
  28. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
  29. package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
  30. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
  31. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
  32. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
  33. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
  34. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
  35. package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
  36. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
  37. package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
  38. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
  39. package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
  40. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +127 -96
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.67.0",
34
+ version: "1.71.0",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
36
  bin: {
37
37
  "create-brainerce-store": "dist/index.js"
@@ -206,7 +206,27 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
206
206
  // dashboard's "orders from ChatGPT/Google/…" reporting. No template code
207
207
  // depends on it (fully automatic), but scaffolds should ship the SDK that
208
208
  // actually reports attribution.
209
- brainerce: "^1.58.0",
209
+ // 1.60 adds marketing.subscribe() — confirmed opt-in for a storefront
210
+ // newsletter signup. The scaffolded footer's signup form calls it, so an
211
+ // older SDK fails type-check at scaffold time.
212
+ // 1.61 adds stockAlerts.subscribe() plus two fields the scaffolded product
213
+ // page reads to decide whether to offer it at all —
214
+ // StoreInfo.stockAlertsEnabled (the merchant's switch) and
215
+ // InventoryInfo.backorderMode (an alert on a backorderable item is a
216
+ // no-op). All three are referenced by the generated back-in-stock form, so
217
+ // an older SDK fails type-check at scaffold time.
218
+ // 1.62 adds the availability clock. The scaffolded DATE/DATETIME checkout
219
+ // pickers pass `{ timezone }` as a third argument to isCalendarDateAllowed()/
220
+ // computeAvailableSlots()/getBusinessHoursForDate() — without it the
221
+ // leadTimeMinutes/cutoffTime/maxDaysAhead bounds are skipped and the picker
222
+ // offers days the API then rejects — and an older SDK types those helpers
223
+ // with two parameters, so the scaffold fails type-check outright.
224
+ // 1.63 adds customer photos on reviews. The scaffolded review form calls
225
+ // uploadReviewPhoto() and passes `imageKeys` on submit, reads `photos` off
226
+ // getMyProductReview() to decide whether to render a picker at all, and the
227
+ // reviews list renders `review.images`. None of those exist on an older SDK,
228
+ // so the scaffold fails type-check outright.
229
+ brainerce: "^1.63.0",
210
230
  "isomorphic-dompurify": "^3.8.0"
211
231
  });
212
232
 
package/messages/en.json CHANGED
@@ -38,7 +38,28 @@
38
38
  "allRightsReserved": "All rights reserved.",
39
39
  "store": "Store",
40
40
  "save": "Save",
41
- "cancel": "Cancel"
41
+ "cancel": "Cancel",
42
+ "birthday": "Birthday",
43
+ "birthdayMonth": "Month",
44
+ "birthdayDay": "Day",
45
+ "birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
46
+ "birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
47
+ "birthdayRequired": "Choose your birthday month and day to continue.",
48
+ "birthdayPlaceholder": "Choose your birthday",
49
+ "birthdayClear": "Clear",
50
+ "birthdayDisplay": "{month} {day}",
51
+ "monthJanuary": "January",
52
+ "monthFebruary": "February",
53
+ "monthMarch": "March",
54
+ "monthApril": "April",
55
+ "monthMay": "May",
56
+ "monthJune": "June",
57
+ "monthJuly": "July",
58
+ "monthAugust": "August",
59
+ "monthSeptember": "September",
60
+ "monthOctober": "October",
61
+ "monthNovember": "November",
62
+ "monthDecember": "December"
42
63
  },
43
64
  "nav": {
44
65
  "products": "Products",
@@ -130,7 +151,13 @@
130
151
  "saving": "Saving…",
131
152
  "submittingBtn": "Submitting…",
132
153
  "saveChanges": "Save changes",
133
- "submitReview": "Submit review"
154
+ "submitReview": "Submit review",
155
+ "addPhotos": "Add photos",
156
+ "removePhoto": "Remove photo",
157
+ "photoUploading": "Uploading…",
158
+ "photoTooLarge": "That photo is too large. Please pick a smaller one.",
159
+ "photoUploadFailed": "Could not upload that photo. Please try another.",
160
+ "photosNeedApproval": "Photos are checked before they appear, so yours may not show right away."
134
161
  },
135
162
  "customization": {
136
163
  "title": "Personalize this product",
@@ -258,6 +285,8 @@
258
285
  "customFieldsDateNextMonth": "Next month",
259
286
  "customFieldsTimeLabel": "Time",
260
287
  "customFieldsTimeClosed": "No delivery times are available on this date.",
288
+ "customFieldsTimeNoneLeft": "Every time on this date has already passed. Please pick a later date.",
289
+ "customFieldsTimeTo": "to",
261
290
  "changeOptions": "Change options",
262
291
  "surcharges": "Additional charges"
263
292
  },
@@ -496,6 +525,27 @@
496
525
  "genericError": "Something went wrong. Please try again.",
497
526
  "fieldRequired": "{label} is required"
498
527
  },
528
+ "backInStock": {
529
+ "title": "Email me when it's back",
530
+ "subtitle": "One message about this item, nothing else. You can stop it any time.",
531
+ "placeholder": "your@email.com",
532
+ "submit": "Notify me",
533
+ "submitting": "Sending...",
534
+ "doneTitle": "We'll email you when it's back",
535
+ "doneBody": "Stock is limited, so it may go quickly once it returns.",
536
+ "genericError": "Something went wrong. Please try again."
537
+ },
538
+ "newsletter": {
539
+ "title": "Join our newsletter",
540
+ "subtitle": "Offers and new arrivals, straight to your inbox.",
541
+ "placeholder": "your@email.com",
542
+ "submit": "Subscribe",
543
+ "submitting": "Sending...",
544
+ "checkEmailTitle": "Almost there — check your email",
545
+ "checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
546
+ "discountCode": "Your code: {code}",
547
+ "genericError": "Something went wrong. Please try again."
548
+ },
499
549
  "content": {
500
550
  "faqTitle": "Frequently Asked Questions",
501
551
  "faqEmptyTitle": "FAQ coming soon",
package/messages/he.json CHANGED
@@ -38,7 +38,28 @@
38
38
  "allRightsReserved": "כל הזכויות שמורות.",
39
39
  "store": "חנות",
40
40
  "save": "שמירה",
41
- "cancel": "ביטול"
41
+ "cancel": "ביטול",
42
+ "birthday": "יום הולדת",
43
+ "birthdayMonth": "חודש",
44
+ "birthdayDay": "יום",
45
+ "birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו.",
46
+ "birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
47
+ "birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
48
+ "birthdayPlaceholder": "בחרו את יום ההולדת",
49
+ "birthdayClear": "ניקוי",
50
+ "birthdayDisplay": "{day} ב{month}",
51
+ "monthJanuary": "ינואר",
52
+ "monthFebruary": "פברואר",
53
+ "monthMarch": "מרץ",
54
+ "monthApril": "אפריל",
55
+ "monthMay": "מאי",
56
+ "monthJune": "יוני",
57
+ "monthJuly": "יולי",
58
+ "monthAugust": "אוגוסט",
59
+ "monthSeptember": "ספטמבר",
60
+ "monthOctober": "אוקטובר",
61
+ "monthNovember": "נובמבר",
62
+ "monthDecember": "דצמבר"
42
63
  },
43
64
  "nav": {
44
65
  "products": "מוצרים",
@@ -130,7 +151,13 @@
130
151
  "saving": "שומר…",
131
152
  "submittingBtn": "שולח…",
132
153
  "saveChanges": "שמירת שינויים",
133
- "submitReview": "שליחת ביקורת"
154
+ "submitReview": "שליחת ביקורת",
155
+ "addPhotos": "הוספת תמונות",
156
+ "removePhoto": "הסרת תמונה",
157
+ "photoUploading": "מעלה...",
158
+ "photoTooLarge": "התמונה הזו גדולה מדי. בחרו תמונה קטנה יותר.",
159
+ "photoUploadFailed": "לא הצלחנו להעלות את התמונה. נסו תמונה אחרת.",
160
+ "photosNeedApproval": "התמונות נבדקות לפני שהן מופיעות, כך שייתכן שהתמונה שלכם לא תוצג מיד."
134
161
  },
135
162
  "customization": {
136
163
  "title": "התאמה אישית של המוצר",
@@ -258,6 +285,8 @@
258
285
  "customFieldsDateNextMonth": "חודש הבא",
259
286
  "customFieldsTimeLabel": "שעה",
260
287
  "customFieldsTimeClosed": "אין שעות משלוח זמינות בתאריך הזה.",
288
+ "customFieldsTimeNoneLeft": "כל השעות בתאריך הזה כבר עברו. בחרו תאריך מאוחר יותר.",
289
+ "customFieldsTimeTo": "עד",
261
290
  "changeOptions": "שנה אפשרויות",
262
291
  "surcharges": "תוספות"
263
292
  },
@@ -496,6 +525,27 @@
496
525
  "genericError": "משהו השתבש. אנא נסה שוב.",
497
526
  "fieldRequired": "השדה {label} הוא חובה"
498
527
  },
528
+ "backInStock": {
529
+ "title": "עדכנו אותי כשחוזר למלאי",
530
+ "subtitle": "הודעה אחת על הפריט הזה, ותו לא. אפשר לבטל בכל רגע.",
531
+ "placeholder": "your@email.com",
532
+ "submit": "עדכנו אותי",
533
+ "submitting": "שולח...",
534
+ "doneTitle": "נעדכן אותך כשהפריט יחזור",
535
+ "doneBody": "הכמות מוגבלת, אז ייתכן שהפריט ייגמר מהר.",
536
+ "genericError": "משהו השתבש. אפשר לנסות שוב."
537
+ },
538
+ "newsletter": {
539
+ "title": "הצטרפו לניוזלטר",
540
+ "subtitle": "מבצעים ומוצרים חדשים, ישירות למייל.",
541
+ "placeholder": "your@email.com",
542
+ "submit": "הרשמה",
543
+ "submitting": "שולח...",
544
+ "checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
545
+ "checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
546
+ "discountCode": "הקוד שלכם: {code}",
547
+ "genericError": "משהו השתבש. נסו שוב."
548
+ },
499
549
  "content": {
500
550
  "faqTitle": "שאלות נפוצות",
501
551
  "faqEmptyTitle": "השאלות הנפוצות בדרך",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.67.0",
3
+ "version": "1.71.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -38,6 +38,13 @@ NEXT_PUBLIC_BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
38
38
  # Set it in your hosting provider's environment variables, not in this file:
39
39
  # .env.local is gitignored and never travels with a deploy.
40
40
  # `NEXT_PUBLIC_SITE_URL` is still read as a backwards-compatible alias.
41
+ #
42
+ # On some hosts SITE_URL is NOT optional. Platforms whose proxy forwards
43
+ # requests to the app as `Host: localhost:<port>` (OpenAI Sites /
44
+ # *.chatgpt.site, among others) hide the real hostname from the server, so
45
+ # nothing can be resolved from the request. Set SITE_URL there, or server-side
46
+ # API calls send `Origin: http://localhost:3000` and a sales channel with a
47
+ # configured Domain rejects them with 403.
41
48
 
42
49
  # Optional: floating WhatsApp contact button (used by some designs, e.g.
43
50
  # passepartout). Digits only, international format without "+"
@@ -81,6 +81,13 @@ development and on any host with nothing configured.
81
81
  `getRequestOrigin()` (for the `Origin` header sent to Brainerce). Hand-rolled
82
82
  versions have shipped `http://` on HTTPS hosts and internal container
83
83
  hostnames.
84
+ - **Some hosts hide the real hostname from the app.** Platforms whose proxy
85
+ forwards requests as `Host: localhost:<port>` with no `x-forwarded-host`
86
+ (OpenAI Sites / `*.chatgpt.site` does this) leave the resolver blind — the
87
+ request looks like it arrived on localhost. On such platforms `SITE_URL` is
88
+ not optional: set it, and the resolver prefers it over any internal host it
89
+ sees. Without it, server-side API calls send `Origin: http://localhost:3000`
90
+ and a channel with a configured Domain rejects them with 403.
84
91
  - **When the merchant gives you a real domain**, set `SITE_URL` in the hosting
85
92
  provider's environment variables — not in `.env.local`, which is gitignored
86
93
  and never travels with a deploy.
@@ -92,6 +92,13 @@ development and on any host with nothing configured.
92
92
  `getRequestOrigin()` (for the `Origin` header sent to Brainerce). Hand-rolled
93
93
  versions have shipped `http://` on HTTPS hosts and internal container
94
94
  hostnames.
95
+ - **Some hosts hide the real hostname from the app.** Platforms whose proxy
96
+ forwards requests as `Host: localhost:<port>` with no `x-forwarded-host`
97
+ (OpenAI Sites / `*.chatgpt.site` does this) leave the resolver blind — the
98
+ request looks like it arrived on localhost. On such platforms `SITE_URL` is
99
+ not optional: set it, and the resolver prefers it over any internal host it
100
+ sees. Without it, server-side API calls send `Origin: http://localhost:3000`
101
+ and a channel with a configured Domain rejects them with 403.
95
102
  - **When the merchant gives you a real domain**, set `SITE_URL` in the hosting
96
103
  provider's environment variables — not in `.env.local`, which is gitignored
97
104
  and never travels with a deploy.
@@ -83,7 +83,10 @@ export default async function BlogPostPage({ params }: PageProps) {
83
83
  const slug = decodeSlug(rawSlug);
84
84
  const client = await getServerClient(locale);
85
85
  const [post, storeInfo] = await Promise.all([
86
- client.blog.getPost(slug).catch(() => null),
86
+ // getPost returns null only on a genuine 404; any other API failure (a
87
+ // 403 from the channel's origin check, a network error) throws and
88
+ // surfaces as the error it is instead of masquerading as a missing post.
89
+ client.blog.getPost(slug),
87
90
  client.getStoreInfo().catch(() => null),
88
91
  ]);
89
92
  if (!post) {
@@ -220,7 +223,10 @@ export default async function BlogPostPage({ params }: PageProps) {
220
223
  const slug = decodeSlug(rawSlug);
221
224
  const client = await getServerClient();
222
225
  const [post, storeInfo] = await Promise.all([
223
- client.blog.getPost(slug).catch(() => null),
226
+ // getPost returns null only on a genuine 404; any other API failure (a
227
+ // 403 from the channel's origin check, a network error) throws and
228
+ // surfaces as the error it is instead of masquerading as a missing post.
229
+ client.blog.getPost(slug),
224
230
  client.getStoreInfo().catch(() => null),
225
231
  ]);
226
232
  if (!post) {
@@ -1,6 +1,6 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { notFound } from 'next/navigation';
3
- import type { Product } from 'brainerce';
3
+ import { BrainerceError } from 'brainerce';
4
4
  import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
5
5
  import { buildMetaDescription } from '@/core/lib/seo';
6
6
  import { sanitizeHtml } from '@/core/lib/sanitize';
@@ -77,8 +77,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
77
77
  images: category.image ? [category.image] : [],
78
78
  },
79
79
  };
80
- } catch {
81
- return { title: 'Category not found' };
80
+ } catch (err) {
81
+ if (err instanceof BrainerceError && err.statusCode === 404) {
82
+ return { title: 'Category not found' };
83
+ }
84
+ // Anything else (channel origin rejection, network failure) hits the page
85
+ // render below too and surfaces there — don't stamp a misleading title.
86
+ return {};
82
87
  }
83
88
  }
84
89
 
@@ -87,14 +92,18 @@ export default async function CategoryPage({ params }: Props) {
87
92
  const slug = decodeSlug(rawSlug);
88
93
  const client = await getServerClient(locale);
89
94
 
90
- const category = await client.getCategoryBySlug(slug).catch(() => null);
95
+ // Only a genuine 404 becomes a storefront 404; a 403 from the channel's
96
+ // origin check or a network failure must surface as the error it is, not
97
+ // masquerade as a missing category (or an empty one).
98
+ const category = await client.getCategoryBySlug(slug).catch((err: unknown) => {
99
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
100
+ throw err;
101
+ });
91
102
  if (!category) notFound();
92
103
 
93
104
  // The listing endpoint owns pagination/FX/publish gating — reuse it rather
94
105
  // than duplicating a product query here.
95
- const { data: products } = await client
96
- .getProducts({ categories: [category.id], limit: 48 })
97
- .catch(() => ({ data: [] as Product[] }));
106
+ const { data: products } = await client.getProducts({ categories: [category.id], limit: 48 });
98
107
 
99
108
  const baseUrl = await getCanonicalSiteUrl();
100
109
  const url = `${baseUrl}/category/${category.slug || slug}`;