create-brainerce-store 1.62.0 → 1.64.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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,276 +1,276 @@
1
- /**
2
- * Blog listing page — fetches published posts from the Brainerce blog API.
3
- *
4
- * Merchants manage posts in the Brainerce dashboard under Sell → Blog.
5
- * The storefront renders them here. Each post links to /blog/[slug].
6
- *
7
- * Posts are only visible when status=PUBLISHED and publishedAt <= NOW().
8
- * Category and tag filtering available via query params.
9
- */
10
- import * as React from 'react';
11
- import type { Metadata } from 'next';
12
- import Link from 'next/link';
13
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
14
-
15
- import { getServerClient } from '@/core/lib/brainerce';
16
-
17
- <% if (i18nEnabled) { %>
18
- type PageProps = {
19
- params: Promise<{ locale: string }>;
20
- searchParams: Promise<{ category?: string; tag?: string; page?: string }>;
21
- };
22
-
23
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
24
- const { locale } = await params;
25
- return {
26
- title: locale === 'he' ? 'בלוג' : 'Blog',
27
- description: locale === 'he' ? 'מאמרים וסיפורים מהחנות' : 'Articles and stories from our store',
28
- };
29
- }
30
-
31
- export default async function BlogListPage({ params, searchParams }: PageProps) {
32
- const { locale } = await params;
33
- const { category, tag, page: pageParam } = await searchParams;
34
- const page = Number(pageParam ?? '1') || 1;
35
-
36
- const client = getServerClient(locale);
37
- const { data: posts, meta } = await client.blog
38
- .getPosts({ category, tag, page, limit: 12 })
39
- .catch(() => ({ data: [], meta: { page: 1, limit: 12, total: 0, totalPages: 1 } }));
40
-
41
- const isHe = locale === 'he';
42
- const dir = isHe ? 'rtl' : 'ltr';
43
- const emptyLabel = isHe ? 'עדיין אין פוסטים.' : 'No posts yet.';
44
- const blogLabel = isHe ? 'בלוג' : 'Blog';
45
- const readMoreLabel = isHe ? 'קרא עוד' : 'Read more';
46
- const prevLabel = isHe ? 'הקודם' : 'Previous';
47
- const nextLabel = isHe ? 'הבא' : 'Next';
48
-
49
- return (
50
- <main dir={dir} className="mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-8">
51
- <h1 className="mb-8 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
52
- {blogLabel}
53
- </h1>
54
-
55
- {posts.length === 0 ? (
56
- <p className="text-muted-foreground">{emptyLabel}</p>
57
- ) : (
58
- <>
59
- <div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
60
- {posts.map((post) => (
61
- <article
62
- key={post.id}
63
- className="group flex flex-col overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm transition-shadow hover:shadow-md"
64
- >
65
- {post.coverImageUrl && (
66
- <Link href={`/blog/${encodeURIComponent(post.slug)}`} tabIndex={-1}>
67
- <div className="relative aspect-[16/9] overflow-hidden">
68
- <Image
69
- src={post.coverImageUrl}
70
- alt={post.coverImageAlt ?? post.title}
71
- fill
72
- className="object-cover transition-transform duration-300 group-hover:scale-105"
73
- sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
74
- />
75
- </div>
76
- </Link>
77
- )}
78
-
79
- <div className="flex flex-1 flex-col gap-3 p-5">
80
- {/* Category badge */}
81
- {post.category && (
82
- <span className="w-fit rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
83
- {post.category}
84
- </span>
85
- )}
86
-
87
- {/* Title */}
88
- <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
89
- <h2 className="line-clamp-2 text-lg font-semibold leading-snug text-foreground hover:text-primary transition-colors">
90
- {post.title}
91
- </h2>
92
- </Link>
93
-
94
- {/* Excerpt */}
95
- {post.excerpt && (
96
- <p className="line-clamp-3 text-sm text-muted-foreground leading-relaxed">
97
- {post.excerpt}
98
- </p>
99
- )}
100
-
101
- {/* Meta: author + date */}
102
- <div className="mt-auto flex items-center gap-2 pt-2 text-xs text-muted-foreground">
103
- {post.author && <span>{post.author}</span>}
104
- {post.author && post.publishedAt && <span>·</span>}
105
- {post.publishedAt && (
106
- <time dateTime={post.publishedAt}>
107
- {new Date(post.publishedAt).toLocaleDateString(isHe ? 'he-IL' : 'en-US', {
108
- year: 'numeric',
109
- month: 'short',
110
- day: 'numeric',
111
- })}
112
- </time>
113
- )}
114
- </div>
115
-
116
- <Link
117
- href={`/blog/${encodeURIComponent(post.slug)}`}
118
- className="mt-1 text-sm font-medium text-primary hover:underline"
119
- >
120
- {readMoreLabel} →
121
- </Link>
122
- </div>
123
- </article>
124
- ))}
125
- </div>
126
-
127
- {/* Pagination */}
128
- {meta.totalPages > 1 && (
129
- <nav className="mt-10 flex items-center justify-center gap-4">
130
- {page > 1 && (
131
- <Link
132
- href={`/blog?page=${page - 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
133
- className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
134
- >
135
- {prevLabel}
136
- </Link>
137
- )}
138
- <span className="text-sm text-muted-foreground">
139
- {page} / {meta.totalPages}
140
- </span>
141
- {page < meta.totalPages && (
142
- <Link
143
- href={`/blog?page=${page + 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
144
- className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
145
- >
146
- {nextLabel}
147
- </Link>
148
- )}
149
- </nav>
150
- )}
151
- </>
152
- )}
153
- </main>
154
- );
155
- }
156
- <% } else { %>
157
- type PageProps = {
158
- searchParams: Promise<{ category?: string; tag?: string; page?: string }>;
159
- };
160
-
161
- export const metadata: Metadata = {
162
- title: 'Blog',
163
- description: 'Articles and stories from our store',
164
- };
165
-
166
- export default async function BlogListPage({ searchParams }: PageProps) {
167
- const { category, tag, page: pageParam } = await searchParams;
168
- const page = Number(pageParam ?? '1') || 1;
169
-
170
- const { data: posts, meta } = await getServerClient()
171
- .blog.getPosts({ category, tag, page, limit: 12 })
172
- .catch(() => ({ data: [], meta: { page: 1, limit: 12, total: 0, totalPages: 1 } }));
173
-
174
- return (
175
- <main className="mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-8">
176
- <h1 className="mb-8 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
177
- Blog
178
- </h1>
179
-
180
- {posts.length === 0 ? (
181
- <p className="text-muted-foreground">No posts yet.</p>
182
- ) : (
183
- <>
184
- <div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
185
- {posts.map((post) => (
186
- <article
187
- key={post.id}
188
- className="group flex flex-col overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm transition-shadow hover:shadow-md"
189
- >
190
- {post.coverImageUrl && (
191
- <Link href={`/blog/${encodeURIComponent(post.slug)}`} tabIndex={-1}>
192
- <div className="relative aspect-[16/9] overflow-hidden">
193
- <Image
194
- src={post.coverImageUrl}
195
- alt={post.coverImageAlt ?? post.title}
196
- fill
197
- className="object-cover transition-transform duration-300 group-hover:scale-105"
198
- sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
199
- />
200
- </div>
201
- </Link>
202
- )}
203
-
204
- <div className="flex flex-1 flex-col gap-3 p-5">
205
- {post.category && (
206
- <span className="w-fit rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
207
- {post.category}
208
- </span>
209
- )}
210
-
211
- <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
212
- <h2 className="line-clamp-2 text-lg font-semibold leading-snug text-foreground hover:text-primary transition-colors">
213
- {post.title}
214
- </h2>
215
- </Link>
216
-
217
- {post.excerpt && (
218
- <p className="line-clamp-3 text-sm text-muted-foreground leading-relaxed">
219
- {post.excerpt}
220
- </p>
221
- )}
222
-
223
- <div className="mt-auto flex items-center gap-2 pt-2 text-xs text-muted-foreground">
224
- {post.author && <span>{post.author}</span>}
225
- {post.author && post.publishedAt && <span>·</span>}
226
- {post.publishedAt && (
227
- <time dateTime={post.publishedAt}>
228
- {new Date(post.publishedAt).toLocaleDateString('en-US', {
229
- year: 'numeric',
230
- month: 'short',
231
- day: 'numeric',
232
- })}
233
- </time>
234
- )}
235
- </div>
236
-
237
- <Link
238
- href={`/blog/${encodeURIComponent(post.slug)}`}
239
- className="mt-1 text-sm font-medium text-primary hover:underline"
240
- >
241
- Read more →
242
- </Link>
243
- </div>
244
- </article>
245
- ))}
246
- </div>
247
-
248
- {meta.totalPages > 1 && (
249
- <nav className="mt-10 flex items-center justify-center gap-4">
250
- {page > 1 && (
251
- <Link
252
- href={`/blog?page=${page - 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
253
- className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
254
- >
255
- Previous
256
- </Link>
257
- )}
258
- <span className="text-sm text-muted-foreground">
259
- {page} / {meta.totalPages}
260
- </span>
261
- {page < meta.totalPages && (
262
- <Link
263
- href={`/blog?page=${page + 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
264
- className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
265
- >
266
- Next
267
- </Link>
268
- )}
269
- </nav>
270
- )}
271
- </>
272
- )}
273
- </main>
274
- );
275
- }
276
- <% } %>
1
+ /**
2
+ * Blog listing page — fetches published posts from the Brainerce blog API.
3
+ *
4
+ * Merchants manage posts in the Brainerce dashboard under Sell → Blog.
5
+ * The storefront renders them here. Each post links to /blog/[slug].
6
+ *
7
+ * Posts are only visible when status=PUBLISHED and publishedAt <= NOW().
8
+ * Category and tag filtering available via query params.
9
+ */
10
+ import * as React from 'react';
11
+ import type { Metadata } from 'next';
12
+ import Link from 'next/link';
13
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
14
+
15
+ import { getServerClient } from '@/core/lib/brainerce';
16
+
17
+ <% if (i18nEnabled) { %>
18
+ type PageProps = {
19
+ params: Promise<{ locale: string }>;
20
+ searchParams: Promise<{ category?: string; tag?: string; page?: string }>;
21
+ };
22
+
23
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
24
+ const { locale } = await params;
25
+ return {
26
+ title: locale === 'he' ? 'בלוג' : 'Blog',
27
+ description: locale === 'he' ? 'מאמרים וסיפורים מהחנות' : 'Articles and stories from our store',
28
+ };
29
+ }
30
+
31
+ export default async function BlogListPage({ params, searchParams }: PageProps) {
32
+ const { locale } = await params;
33
+ const { category, tag, page: pageParam } = await searchParams;
34
+ const page = Number(pageParam ?? '1') || 1;
35
+
36
+ const client = getServerClient(locale);
37
+ const { data: posts, meta } = await client.blog
38
+ .getPosts({ category, tag, page, limit: 12 })
39
+ .catch(() => ({ data: [], meta: { page: 1, limit: 12, total: 0, totalPages: 1 } }));
40
+
41
+ const isHe = locale === 'he';
42
+ const dir = isHe ? 'rtl' : 'ltr';
43
+ const emptyLabel = isHe ? 'עדיין אין פוסטים.' : 'No posts yet.';
44
+ const blogLabel = isHe ? 'בלוג' : 'Blog';
45
+ const readMoreLabel = isHe ? 'קרא עוד' : 'Read more';
46
+ const prevLabel = isHe ? 'הקודם' : 'Previous';
47
+ const nextLabel = isHe ? 'הבא' : 'Next';
48
+
49
+ return (
50
+ <main dir={dir} className="mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-8">
51
+ <h1 className="mb-8 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
52
+ {blogLabel}
53
+ </h1>
54
+
55
+ {posts.length === 0 ? (
56
+ <p className="text-muted-foreground">{emptyLabel}</p>
57
+ ) : (
58
+ <>
59
+ <div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
60
+ {posts.map((post) => (
61
+ <article
62
+ key={post.id}
63
+ className="group flex flex-col overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm transition-shadow hover:shadow-md"
64
+ >
65
+ {post.coverImageUrl && (
66
+ <Link href={`/blog/${encodeURIComponent(post.slug)}`} tabIndex={-1}>
67
+ <div className="relative aspect-[16/9] overflow-hidden">
68
+ <Image
69
+ src={post.coverImageUrl}
70
+ alt={post.coverImageAlt ?? post.title}
71
+ fill
72
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
73
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
74
+ />
75
+ </div>
76
+ </Link>
77
+ )}
78
+
79
+ <div className="flex flex-1 flex-col gap-3 p-5">
80
+ {/* Category badge */}
81
+ {post.category && (
82
+ <span className="w-fit rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
83
+ {post.category}
84
+ </span>
85
+ )}
86
+
87
+ {/* Title */}
88
+ <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
89
+ <h2 className="line-clamp-2 text-lg font-semibold leading-snug text-foreground hover:text-primary transition-colors">
90
+ {post.title}
91
+ </h2>
92
+ </Link>
93
+
94
+ {/* Excerpt */}
95
+ {post.excerpt && (
96
+ <p className="line-clamp-3 text-sm text-muted-foreground leading-relaxed">
97
+ {post.excerpt}
98
+ </p>
99
+ )}
100
+
101
+ {/* Meta: author + date */}
102
+ <div className="mt-auto flex items-center gap-2 pt-2 text-xs text-muted-foreground">
103
+ {post.author && <span>{post.author}</span>}
104
+ {post.author && post.publishedAt && <span>·</span>}
105
+ {post.publishedAt && (
106
+ <time dateTime={post.publishedAt}>
107
+ {new Date(post.publishedAt).toLocaleDateString(isHe ? 'he-IL' : 'en-US', {
108
+ year: 'numeric',
109
+ month: 'short',
110
+ day: 'numeric',
111
+ })}
112
+ </time>
113
+ )}
114
+ </div>
115
+
116
+ <Link
117
+ href={`/blog/${encodeURIComponent(post.slug)}`}
118
+ className="mt-1 text-sm font-medium text-primary hover:underline"
119
+ >
120
+ {readMoreLabel} →
121
+ </Link>
122
+ </div>
123
+ </article>
124
+ ))}
125
+ </div>
126
+
127
+ {/* Pagination */}
128
+ {meta.totalPages > 1 && (
129
+ <nav className="mt-10 flex items-center justify-center gap-4">
130
+ {page > 1 && (
131
+ <Link
132
+ href={`/blog?page=${page - 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
133
+ className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
134
+ >
135
+ {prevLabel}
136
+ </Link>
137
+ )}
138
+ <span className="text-sm text-muted-foreground">
139
+ {page} / {meta.totalPages}
140
+ </span>
141
+ {page < meta.totalPages && (
142
+ <Link
143
+ href={`/blog?page=${page + 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
144
+ className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
145
+ >
146
+ {nextLabel}
147
+ </Link>
148
+ )}
149
+ </nav>
150
+ )}
151
+ </>
152
+ )}
153
+ </main>
154
+ );
155
+ }
156
+ <% } else { %>
157
+ type PageProps = {
158
+ searchParams: Promise<{ category?: string; tag?: string; page?: string }>;
159
+ };
160
+
161
+ export const metadata: Metadata = {
162
+ title: 'Blog',
163
+ description: 'Articles and stories from our store',
164
+ };
165
+
166
+ export default async function BlogListPage({ searchParams }: PageProps) {
167
+ const { category, tag, page: pageParam } = await searchParams;
168
+ const page = Number(pageParam ?? '1') || 1;
169
+
170
+ const { data: posts, meta } = await getServerClient()
171
+ .blog.getPosts({ category, tag, page, limit: 12 })
172
+ .catch(() => ({ data: [], meta: { page: 1, limit: 12, total: 0, totalPages: 1 } }));
173
+
174
+ return (
175
+ <main className="mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-8">
176
+ <h1 className="mb-8 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
177
+ Blog
178
+ </h1>
179
+
180
+ {posts.length === 0 ? (
181
+ <p className="text-muted-foreground">No posts yet.</p>
182
+ ) : (
183
+ <>
184
+ <div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
185
+ {posts.map((post) => (
186
+ <article
187
+ key={post.id}
188
+ className="group flex flex-col overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm transition-shadow hover:shadow-md"
189
+ >
190
+ {post.coverImageUrl && (
191
+ <Link href={`/blog/${encodeURIComponent(post.slug)}`} tabIndex={-1}>
192
+ <div className="relative aspect-[16/9] overflow-hidden">
193
+ <Image
194
+ src={post.coverImageUrl}
195
+ alt={post.coverImageAlt ?? post.title}
196
+ fill
197
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
198
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
199
+ />
200
+ </div>
201
+ </Link>
202
+ )}
203
+
204
+ <div className="flex flex-1 flex-col gap-3 p-5">
205
+ {post.category && (
206
+ <span className="w-fit rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
207
+ {post.category}
208
+ </span>
209
+ )}
210
+
211
+ <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
212
+ <h2 className="line-clamp-2 text-lg font-semibold leading-snug text-foreground hover:text-primary transition-colors">
213
+ {post.title}
214
+ </h2>
215
+ </Link>
216
+
217
+ {post.excerpt && (
218
+ <p className="line-clamp-3 text-sm text-muted-foreground leading-relaxed">
219
+ {post.excerpt}
220
+ </p>
221
+ )}
222
+
223
+ <div className="mt-auto flex items-center gap-2 pt-2 text-xs text-muted-foreground">
224
+ {post.author && <span>{post.author}</span>}
225
+ {post.author && post.publishedAt && <span>·</span>}
226
+ {post.publishedAt && (
227
+ <time dateTime={post.publishedAt}>
228
+ {new Date(post.publishedAt).toLocaleDateString('en-US', {
229
+ year: 'numeric',
230
+ month: 'short',
231
+ day: 'numeric',
232
+ })}
233
+ </time>
234
+ )}
235
+ </div>
236
+
237
+ <Link
238
+ href={`/blog/${encodeURIComponent(post.slug)}`}
239
+ className="mt-1 text-sm font-medium text-primary hover:underline"
240
+ >
241
+ Read more →
242
+ </Link>
243
+ </div>
244
+ </article>
245
+ ))}
246
+ </div>
247
+
248
+ {meta.totalPages > 1 && (
249
+ <nav className="mt-10 flex items-center justify-center gap-4">
250
+ {page > 1 && (
251
+ <Link
252
+ href={`/blog?page=${page - 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
253
+ className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
254
+ >
255
+ Previous
256
+ </Link>
257
+ )}
258
+ <span className="text-sm text-muted-foreground">
259
+ {page} / {meta.totalPages}
260
+ </span>
261
+ {page < meta.totalPages && (
262
+ <Link
263
+ href={`/blog?page=${page + 1}${category ? `&category=${category}` : ''}${tag ? `&tag=${tag}` : ''}`}
264
+ className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted transition-colors"
265
+ >
266
+ Next
267
+ </Link>
268
+ )}
269
+ </nav>
270
+ )}
271
+ </>
272
+ )}
273
+ </main>
274
+ );
275
+ }
276
+ <% } %>
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Blog RSS feed — GET /blog/rss.xml
3
+ *
4
+ * Syndication for the store's articles (many of them auto-published by the
5
+ * SEO Autopilot): feed readers, newsletter tools, and aggregators can follow
6
+ * the blog without polling HTML. The root layout advertises it via
7
+ * <link rel="alternate" type="application/rss+xml">.
8
+ *
9
+ * NOTE (multi-locale scaffolds): the URL contains a dot, so the locale
10
+ * middleware never rewrites it, and a copy nested under [locale]/blog/ would
11
+ * 404. The scaffolder moves this route back to the app ROOT
12
+ * (app/blog/rss.xml) after the [locale] move — the literal `blog` segment
13
+ * wins over `[locale]` for /blog/rss.xml, while /he/blog/... keeps matching
14
+ * the locale tree. See scaffold.ts.
15
+ */
16
+ import { getServerClient } from '@/core/lib/brainerce';
17
+
18
+ export const revalidate = 3600;
19
+
20
+ function esc(value: string): string {
21
+ return value
22
+ .replace(/&/g, '&amp;')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;');
26
+ }
27
+
28
+ export async function GET() {
29
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
30
+ const client = getServerClient();
31
+
32
+ const [info, posts] = await Promise.all([
33
+ client.getStoreInfo().catch(() => null),
34
+ client.blog.getPosts({ limit: 50 }).catch(() => null),
35
+ ]);
36
+
37
+ const items = (posts?.data ?? [])
38
+ .map((post) => {
39
+ const url = `${baseUrl}/blog/${post.slug}`;
40
+ const description = post.seoDescription ?? post.excerpt ?? '';
41
+ const pubDate = post.publishedAt ? new Date(post.publishedAt).toUTCString() : undefined;
42
+ return [
43
+ ' <item>',
44
+ ` <title>${esc(post.title)}</title>`,
45
+ ` <link>${esc(url)}</link>`,
46
+ ` <guid isPermaLink="true">${esc(url)}</guid>`,
47
+ ...(pubDate ? [` <pubDate>${pubDate}</pubDate>`] : []),
48
+ ...(description ? [` <description>${esc(description)}</description>`] : []),
49
+ ' </item>',
50
+ ].join('\n');
51
+ })
52
+ .join('\n');
53
+
54
+ const xml = [
55
+ '<?xml version="1.0" encoding="UTF-8"?>',
56
+ '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
57
+ ' <channel>',
58
+ ` <title>${esc(info?.name ?? 'Blog')}</title>`,
59
+ ` <link>${esc(`${baseUrl}/blog`)}</link>`,
60
+ ` <description>${esc(info?.metaDescription ?? `${info?.name ?? 'Store'} blog`)}</description>`,
61
+ ` <atom:link href="${esc(`${baseUrl}/blog/rss.xml`)}" rel="self" type="application/rss+xml"/>`,
62
+ items,
63
+ ' </channel>',
64
+ '</rss>',
65
+ ].join('\n');
66
+
67
+ return new Response(xml, {
68
+ headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
69
+ });
70
+ }
@@ -1,7 +1,7 @@
1
- 'use client';
2
-
3
- import { CartView } from '@/ui/cart/cart-view';
4
-
5
- export default function CartPage() {
6
- return <CartView />;
7
- }
1
+ 'use client';
2
+
3
+ import { CartView } from '@/ui/cart/cart-view';
4
+
5
+ export default function CartPage() {
6
+ return <CartView />;
7
+ }