create-brainerce-store 1.51.0 → 1.52.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 (170) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +9 -68
  3. package/messages/en.json +32 -0
  4. package/messages/he.json +32 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +9 -0
  7. package/templates/nextjs/base/AI-GUIDE.md +52 -3
  8. package/templates/nextjs/base/CLAUDE.md +10 -0
  9. package/templates/nextjs/base/package.json.ejs +52 -39
  10. package/templates/nextjs/base/src/app/globals.css +36 -30
  11. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
  12. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  13. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  14. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  15. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  16. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  17. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  18. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  19. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  20. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  21. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  22. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  23. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  24. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  25. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  26. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  27. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  28. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
  29. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
  30. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
  32. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
  33. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
  34. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
  35. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
  37. package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
  38. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
  39. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
  40. package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
  41. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
  42. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
  43. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
  44. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
  45. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
  46. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
  47. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
  48. package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
  49. package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
  50. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
  51. package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
  52. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
  53. package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
  54. package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
  55. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
  56. package/templates/nextjs/base/tailwind.config.ts +69 -42
  57. package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
  58. package/templates/nextjs/designs/atelier/globals.css +396 -0
  59. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  60. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  61. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
  62. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  63. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  64. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  65. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  66. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
  67. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  68. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  69. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
  70. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
  71. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  72. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  73. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  74. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  75. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  76. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  77. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  78. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  79. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  80. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  81. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  82. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  83. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  84. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  85. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  86. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  87. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  88. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  89. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  90. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
  91. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
  92. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  93. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  94. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  95. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  96. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  97. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  98. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  99. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
  100. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  101. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  102. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  103. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  104. package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
  105. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  106. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  107. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  108. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  109. package/templates/nextjs/themes/luxury/globals.css +405 -399
  110. package/templates/nextjs/themes/minimal/globals.css +36 -30
  111. package/templates/nextjs/themes/playful/globals.css +406 -400
  112. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  115. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  118. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  119. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  120. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  121. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  123. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  124. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  126. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  127. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  128. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  129. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  130. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  131. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  132. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  135. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  136. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  137. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  139. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  140. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  141. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  142. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  143. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  144. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  145. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  146. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  147. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  148. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  149. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  150. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  151. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  152. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  153. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  154. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  155. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  156. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  157. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  158. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  159. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  160. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  161. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  162. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  163. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  164. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  165. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  166. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  167. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  168. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  169. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  170. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
@@ -1,164 +1,144 @@
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 are inline SVGs so the
9
- * scaffold stays dependency-free; unknown platforms fall back to a
10
- * 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 type { Content } from 'brainerce';
19
-
20
- interface SiteFooterProps {
21
- /** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
22
- footer: Content<'FOOTER'> | null;
23
- /** Store name shown in the brand block + fallback copyright. */
24
- storeName?: string;
25
- }
26
-
27
- type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
28
-
29
- const InstagramIcon: SvgComponent = (props) => (
30
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
31
- <rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
32
- <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
33
- <line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
34
- </svg>
35
- );
36
-
37
- const FacebookIcon: SvgComponent = (props) => (
38
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
39
- <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
40
- </svg>
41
- );
42
-
43
- const XIcon: SvgComponent = (props) => (
44
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
45
- <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" />
46
- </svg>
47
- );
48
-
49
- const LinkedInIcon: SvgComponent = (props) => (
50
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
51
- <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.852 3.37-1.852 3.601 0 4.267 2.37 4.267 5.455v6.288zM5.337 7.433a2.062 2.062 0 1 1 0-4.125 2.063 2.063 0 0 1 0 4.125zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z" />
52
- </svg>
53
- );
54
-
55
- const YouTubeIcon: SvgComponent = (props) => (
56
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
57
- <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
58
- </svg>
59
- );
60
-
61
- const TikTokIcon: SvgComponent = (props) => (
62
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
63
- <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" />
64
- </svg>
65
- );
66
-
67
- const SOCIAL_ICONS: Record<string, SvgComponent> = {
68
- instagram: InstagramIcon,
69
- facebook: FacebookIcon,
70
- x: XIcon,
71
- twitter: XIcon,
72
- linkedin: LinkedInIcon,
73
- youtube: YouTubeIcon,
74
- tiktok: TikTokIcon,
75
- };
76
-
77
- export function SiteFooter({ footer, storeName }: SiteFooterProps) {
78
- const data = footer?.data;
79
- const columns = data?.columns ?? [];
80
- const social = data?.social ?? [];
81
- const year = new Date().getFullYear();
82
- const brandLabel = storeName ?? 'Store';
83
-
84
- if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
85
- return (
86
- <footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
87
- <div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
88
- © {year} {brandLabel}. All rights reserved.
89
- </div>
90
- </footer>
91
- );
92
- }
93
-
94
- return (
95
- <footer className="border-border bg-muted/30 mt-16 border-t">
96
- <div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
97
- <div className="grid grid-cols-1 gap-10 md:grid-cols-12">
98
- <div className="md:col-span-4">
99
- <a
100
- href="/"
101
- className="text-foreground text-lg font-semibold tracking-tight"
102
- >
103
- {brandLabel}
104
- </a>
105
- {social.length > 0 ? (
106
- <div className="mt-4 flex flex-wrap items-center gap-2">
107
- {social.map((entry, idx) => {
108
- const key = entry.platform.toLowerCase();
109
- const Icon = SOCIAL_ICONS[key];
110
- return (
111
- <a
112
- key={`${entry.platform}-${idx}`}
113
- href={entry.url}
114
- target="_blank"
115
- rel="noopener noreferrer"
116
- aria-label={entry.platform}
117
- 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"
118
- >
119
- {Icon ? (
120
- <Icon className="h-4 w-4" />
121
- ) : (
122
- <span className="text-xs font-medium uppercase">
123
- {entry.platform.charAt(0)}
124
- </span>
125
- )}
126
- </a>
127
- );
128
- })}
129
- </div>
130
- ) : null}
131
- </div>
132
-
133
- {columns.length > 0 ? (
134
- <div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
135
- {columns.map((col, idx) => (
136
- <div key={`${col.title}-${idx}`}>
137
- <h3 className="text-foreground mb-3 text-sm font-semibold tracking-wide">
138
- {col.title}
139
- </h3>
140
- <ul className="space-y-2">
141
- {col.links.map((link, linkIdx) => (
142
- <li key={`${link.url}-${linkIdx}`}>
143
- <a
144
- href={link.url}
145
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
146
- >
147
- {link.label}
148
- </a>
149
- </li>
150
- ))}
151
- </ul>
152
- </div>
153
- ))}
154
- </div>
155
- ) : null}
156
- </div>
157
-
158
- <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">
159
- <span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
160
- </div>
161
- </div>
162
- </footer>
163
- );
164
- }
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
+
25
+ interface SiteFooterProps {
26
+ /** Pre-fetched footer payload (server-side). `null` triggers fallback rendering. */
27
+ footer: Content<'FOOTER'> | null;
28
+ /** Store name shown in the brand block + fallback copyright. */
29
+ storeName?: string;
30
+ }
31
+
32
+ type SvgComponent = React.ComponentType<React.SVGProps<SVGSVGElement>>;
33
+
34
+ // No lucide equivalent — keep as local SVGs.
35
+ const XIcon: SvgComponent = (props) => (
36
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
37
+ <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" />
38
+ </svg>
39
+ );
40
+
41
+ const TikTokIcon: SvgComponent = (props) => (
42
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
43
+ <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" />
44
+ </svg>
45
+ );
46
+
47
+ const SOCIAL_ICONS: Record<string, SvgComponent> = {
48
+ instagram: Instagram,
49
+ facebook: Facebook,
50
+ x: XIcon,
51
+ twitter: XIcon,
52
+ linkedin: Linkedin,
53
+ youtube: Youtube,
54
+ tiktok: TikTokIcon,
55
+ };
56
+
57
+ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
58
+ const data = footer?.data;
59
+ const columns = data?.columns ?? [];
60
+ const social = data?.social ?? [];
61
+ const year = new Date().getFullYear();
62
+ const brandLabel = storeName ?? 'Store';
63
+
64
+ if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
65
+ return (
66
+ <footer className="border-border bg-muted/30 text-muted-foreground mt-16 border-t py-8 text-sm">
67
+ <div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
68
+ © {year} {brandLabel}. All rights reserved.
69
+ </div>
70
+ </footer>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <footer className="border-border bg-muted/30 mt-16 border-t">
76
+ <div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
77
+ <div className="grid grid-cols-1 gap-10 md:grid-cols-12">
78
+ <div className="md:col-span-4">
79
+ <Link
80
+ href="/"
81
+ className="text-foreground text-lg font-semibold tracking-tight"
82
+ >
83
+ {brandLabel}
84
+ </Link>
85
+ {social.length > 0 ? (
86
+ <div className="mt-4 flex flex-wrap items-center gap-2">
87
+ {social.map((entry, idx) => {
88
+ const key = entry.platform.toLowerCase();
89
+ const Icon = SOCIAL_ICONS[key];
90
+ return (
91
+ <a
92
+ key={`${entry.platform}-${idx}`}
93
+ href={entry.url}
94
+ target="_blank"
95
+ rel="noopener noreferrer"
96
+ aria-label={entry.platform}
97
+ 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"
98
+ >
99
+ {Icon ? (
100
+ <Icon className="h-4 w-4" />
101
+ ) : (
102
+ <span className="text-xs font-medium uppercase">
103
+ {entry.platform.charAt(0)}
104
+ </span>
105
+ )}
106
+ </a>
107
+ );
108
+ })}
109
+ </div>
110
+ ) : null}
111
+ </div>
112
+
113
+ {columns.length > 0 ? (
114
+ <div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-8">
115
+ {columns.map((col, idx) => (
116
+ <div key={`${col.title}-${idx}`}>
117
+ <h3 className="text-foreground mb-3 text-sm font-semibold tracking-wide">
118
+ {col.title}
119
+ </h3>
120
+ <ul className="space-y-2">
121
+ {col.links.map((link, linkIdx) => (
122
+ <li key={`${link.url}-${linkIdx}`}>
123
+ <a
124
+ href={link.url}
125
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
126
+ >
127
+ {link.label}
128
+ </a>
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ ) : null}
136
+ </div>
137
+
138
+ <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">
139
+ <span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved.`}</span>
140
+ </div>
141
+ </div>
142
+ </footer>
143
+ );
144
+ }