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,160 +1,160 @@
1
- /**
2
- * Storefront footer — fully driven by the merchant's dashboard configuration.
3
- *
4
- * The merchant configures the footer in the Brainerce dashboard under
5
- * Sell → Content → Footer
6
- *
7
- * Renders a brand block (store-name + social icons), the merchant's link
8
- * columns, and a copyright bar. Social icons come from lucide-react where
9
- * available; X (Twitter) and TikTok have no lucide glyph, so those two stay
10
- * as local inline SVGs. Unknown platforms fall back to a letter avatar.
11
- *
12
- * API contract:
13
- * GET → client.content.footer.get('main', locale) → Content<'FOOTER'> | null
14
- * Returns null on 404 — we render a minimal fallback so the layout never
15
- * crashes when the merchant hasn't seeded the footer yet.
16
- */
17
- import * as React from 'react';
18
- import { Facebook, Instagram, Linkedin, Youtube } from 'lucide-react';
19
- import type { Content } from 'brainerce';
20
- // Locale-aware Link (client component — fine to render from this server
21
- // component). Merchant-configured link columns keep plain <a> since their
22
- // URLs are arbitrary (may be external).
23
- import { Link } from '@/core/lib/navigation';
24
- // Client component — the signup posts from the browser, which is also what
25
- // satisfies the endpoint's browser-Origin requirement.
26
- import { NewsletterSignup } from './newsletter-signup';
27
-
28
- interface SiteFooterProps {
29
- /** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
30
- footer: Content<'FOOTER'> | null;
31
- /** Store name shown in the brand block + fallback copyright. */
32
- storeName?: string;
33
- }
34
-
35
- type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
36
-
37
- // No lucide equivalent — keep as local SVGs.
38
- const XIcon: SvgComponent = (props) => (
39
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
40
- <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
41
- </svg>
42
- );
43
-
44
- const TikTokIcon: SvgComponent = (props) => (
45
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
46
- <path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z" />
47
- </svg>
48
- );
49
-
50
- const SOCIAL_ICONS: Record<string, SvgComponent> = {
51
- instagram: Instagram,
52
- facebook: Facebook,
53
- x: XIcon,
54
- twitter: XIcon,
55
- linkedin: Linkedin,
56
- youtube: Youtube,
57
- tiktok: TikTokIcon,
58
- };
59
-
60
- export function SiteFooter({ footer, storeName }: SiteFooterProps) {
61
- const data = footer?.data;
62
- const columns = data?.columns ?? [];
63
- const social = data?.social ?? [];
64
- const year = new Date().getFullYear();
65
- const brandLabel = storeName ?? 'Store';
66
-
67
- if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
68
- return (
69
- <footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
70
- <div className="mx-auto max-w-7xl space-y-6 px-4 text-center sm:px-6 lg:px-8">
71
- <div className="mx-auto max-w-md text-start">
72
- <NewsletterSignup />
73
- </div>
74
- <div>© {year} {brandLabel}. All rights reserved.</div>
75
- </div>
76
- </footer>
77
- );
78
- }
79
-
80
- return (
81
- <footer className="border-border bg-muted/30 mt-16 border-t">
82
- <div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
83
- <div className="grid grid-cols-1 gap-10 md:grid-cols-12">
84
- <div className="md:col-span-4">
85
- <Link
86
- href="/"
87
- className="text-foreground text-lg font-semibold tracking-tight"
88
- >
89
- {brandLabel}
90
- </Link>
91
- {social.length > 0 ? (
92
- <div className="mt-4 flex flex-wrap items-center gap-2">
93
- {social.map((entry, idx) => {
94
- const key = entry.platform.toLowerCase();
95
- const Icon = SOCIAL_ICONS[key];
96
- return (
97
- <a
98
- key={`${entry.platform}-${idx}`}
99
- href={entry.url}
100
- target="_blank"
101
- rel="noopener noreferrer"
102
- aria-label={entry.platform}
103
- className="border-border hover:border-foreground/40 hover:text-foreground text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors"
104
- >
105
- {Icon ? (
106
- <Icon className="h-4 w-4" />
107
- ) : (
108
- <span className="text-xs font-medium uppercase">
109
- {entry.platform.charAt(0)}
110
- </span>
111
- )}
112
- </a>
113
- );
114
- })}
115
- </div>
116
- ) : null}
117
-
118
- {/*
119
- Newsletter signup. Remove this block if the merchant does not want
120
- a mailing list — nothing else depends on it. Confirmed opt-in: the
121
- address is not subscribed until the recipient clicks the link they
122
- are mailed, which is why the success copy says "check your email".
123
- */}
124
- <div className="mt-8">
125
- <NewsletterSignup />
126
- </div>
127
- </div>
128
-
129
- {columns.length > 0 ? (
130
- <div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
131
- {columns.map((col, idx) => (
132
- <div key={`${col.title}-${idx}`}>
133
- <h3 className="text-foreground mb-3 text-sm font-semibold tracking-wide">
134
- {col.title}
135
- </h3>
136
- <ul className="space-y-2">
137
- {col.links.map((link, linkIdx) => (
138
- <li key={`${link.url}-${linkIdx}`}>
139
- <a
140
- href={link.url}
141
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
142
- >
143
- {link.label}
144
- </a>
145
- </li>
146
- ))}
147
- </ul>
148
- </div>
149
- ))}
150
- </div>
151
- ) : null}
152
- </div>
153
-
154
- <div className="border-border text-muted-foreground mt-10 flex flex-col items-center justify-between gap-2 border-t pt-6 text-xs sm:flex-row">
155
- <span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
156
- </div>
157
- </div>
158
- </footer>
159
- );
160
- }
1
+ /**
2
+ * Storefront footer — fully driven by the merchant's dashboard configuration.
3
+ *
4
+ * The merchant configures the footer in the Brainerce dashboard under
5
+ * Sell → Content → Footer
6
+ *
7
+ * Renders a brand block (store-name + social icons), the merchant's link
8
+ * columns, and a copyright bar. Social icons come from lucide-react where
9
+ * available; X (Twitter) and TikTok have no lucide glyph, so those two stay
10
+ * as local inline SVGs. Unknown platforms fall back to a letter avatar.
11
+ *
12
+ * API contract:
13
+ * GET → client.content.footer.get('main', locale) → Content<'FOOTER'> | null
14
+ * Returns null on 404 — we render a minimal fallback so the layout never
15
+ * crashes when the merchant hasn't seeded the footer yet.
16
+ */
17
+ import * as React from 'react';
18
+ import { Facebook, Instagram, Linkedin, Youtube } from 'lucide-react';
19
+ import type { Content } from 'brainerce';
20
+ // Locale-aware Link (client component — fine to render from this server
21
+ // component). Merchant-configured link columns keep plain <a> since their
22
+ // URLs are arbitrary (may be external).
23
+ import { Link } from '@/core/lib/navigation';
24
+ // Client component — the signup posts from the browser, which is also what
25
+ // satisfies the endpoint's browser-Origin requirement.
26
+ import { NewsletterSignup } from './newsletter-signup';
27
+
28
+ interface SiteFooterProps {
29
+ /** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
30
+ footer: Content<'FOOTER'> | null;
31
+ /** Store name shown in the brand block + fallback copyright. */
32
+ storeName?: string;
33
+ }
34
+
35
+ type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
36
+
37
+ // No lucide equivalent — keep as local SVGs.
38
+ const XIcon: SvgComponent = (props) => (
39
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
40
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
41
+ </svg>
42
+ );
43
+
44
+ const TikTokIcon: SvgComponent = (props) => (
45
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
46
+ <path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z" />
47
+ </svg>
48
+ );
49
+
50
+ const SOCIAL_ICONS: Record<string, SvgComponent> = {
51
+ instagram: Instagram,
52
+ facebook: Facebook,
53
+ x: XIcon,
54
+ twitter: XIcon,
55
+ linkedin: Linkedin,
56
+ youtube: Youtube,
57
+ tiktok: TikTokIcon,
58
+ };
59
+
60
+ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
61
+ const data = footer?.data;
62
+ const columns = data?.columns ?? [];
63
+ const social = data?.social ?? [];
64
+ const year = new Date().getFullYear();
65
+ const brandLabel = storeName ?? 'Store';
66
+
67
+ if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
68
+ return (
69
+ <footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
70
+ <div className="mx-auto max-w-7xl space-y-6 px-4 text-center sm:px-6 lg:px-8">
71
+ <div className="mx-auto max-w-md text-start">
72
+ <NewsletterSignup />
73
+ </div>
74
+ <div>© {year} {brandLabel}. All rights reserved.</div>
75
+ </div>
76
+ </footer>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <footer className="border-border bg-muted/30 mt-16 border-t">
82
+ <div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
83
+ <div className="grid grid-cols-1 gap-10 md:grid-cols-12">
84
+ <div className="md:col-span-4">
85
+ <Link
86
+ href="/"
87
+ className="text-foreground text-lg font-semibold ltr:tracking-tight"
88
+ >
89
+ {brandLabel}
90
+ </Link>
91
+ {social.length > 0 ? (
92
+ <div className="mt-4 flex flex-wrap items-center gap-2">
93
+ {social.map((entry, idx) => {
94
+ const key = entry.platform.toLowerCase();
95
+ const Icon = SOCIAL_ICONS[key];
96
+ return (
97
+ <a
98
+ key={`${entry.platform}-${idx}`}
99
+ href={entry.url}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ aria-label={entry.platform}
103
+ className="border-border hover:border-foreground/40 hover:text-foreground text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors"
104
+ >
105
+ {Icon ? (
106
+ <Icon className="h-4 w-4" />
107
+ ) : (
108
+ <span className="text-xs font-medium uppercase">
109
+ {entry.platform.charAt(0)}
110
+ </span>
111
+ )}
112
+ </a>
113
+ );
114
+ })}
115
+ </div>
116
+ ) : null}
117
+
118
+ {/*
119
+ Newsletter signup. Remove this block if the merchant does not want
120
+ a mailing list — nothing else depends on it. Confirmed opt-in: the
121
+ address is not subscribed until the recipient clicks the link they
122
+ are mailed, which is why the success copy says "check your email".
123
+ */}
124
+ <div className="mt-8">
125
+ <NewsletterSignup />
126
+ </div>
127
+ </div>
128
+
129
+ {columns.length > 0 ? (
130
+ <div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
131
+ {columns.map((col, idx) => (
132
+ <div key={`${col.title}-${idx}`}>
133
+ <h3 className="text-foreground mb-3 text-sm font-semibold ltr:tracking-wide">
134
+ {col.title}
135
+ </h3>
136
+ <ul className="space-y-2">
137
+ {col.links.map((link, linkIdx) => (
138
+ <li key={`${link.url}-${linkIdx}`}>
139
+ <a
140
+ href={link.url}
141
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
142
+ >
143
+ {link.label}
144
+ </a>
145
+ </li>
146
+ ))}
147
+ </ul>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+
154
+ <div className="border-border text-muted-foreground mt-10 flex flex-col items-center justify-between gap-2 border-t pt-6 text-xs sm:flex-row">
155
+ <span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
156
+ </div>
157
+ </div>
158
+ </footer>
159
+ );
160
+ }
@@ -53,7 +53,7 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
53
53
  return (
54
54
  <header className="border-border bg-background/95 supports-[backdrop-filter]:bg-background/70 sticky top-0 z-40 border-b backdrop-blur">
55
55
  <div className="mx-auto flex h-16 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
56
- <Link href="/" className="text-lg font-semibold tracking-tight" aria-label={brandLabel}>
56
+ <Link href="/" className="text-lg font-semibold ltr:tracking-tight" aria-label={brandLabel}>
57
57
  {brandLabel}
58
58
  </Link>
59
59
  {/* Search lives in the fallback branch too — a store whose merchant
@@ -88,7 +88,7 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
88
88
  <div className="mx-auto flex h-16 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
89
89
  <Link
90
90
  href="/"
91
- className="flex items-center gap-2 font-semibold tracking-tight"
91
+ className="flex items-center gap-2 font-semibold ltr:tracking-tight"
92
92
  aria-label={brandLabel}
93
93
  >
94
94
  {logo ? (