@rovela-ai/sdk 0.19.4 → 0.19.6

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 (236) hide show
  1. package/dist/admin/components/AdminPageHeader.d.ts.map +1 -1
  2. package/dist/admin/components/AdminPageHeader.js +9 -2
  3. package/dist/admin/components/AdminPageHeader.js.map +1 -1
  4. package/dist/admin/components/AdminUserMenu.d.ts.map +1 -1
  5. package/dist/admin/components/AdminUserMenu.js +2 -1
  6. package/dist/admin/components/AdminUserMenu.js.map +1 -1
  7. package/dist/admin/components/CustomerDetails.d.ts.map +1 -1
  8. package/dist/admin/components/CustomerDetails.js +2 -1
  9. package/dist/admin/components/CustomerDetails.js.map +1 -1
  10. package/dist/admin/components/EmailsManager.d.ts.map +1 -1
  11. package/dist/admin/components/EmailsManager.js +2 -1
  12. package/dist/admin/components/EmailsManager.js.map +1 -1
  13. package/dist/admin/components/LowStockAlert.d.ts.map +1 -1
  14. package/dist/admin/components/LowStockAlert.js +3 -2
  15. package/dist/admin/components/LowStockAlert.js.map +1 -1
  16. package/dist/admin/components/OrderDetails.d.ts.map +1 -1
  17. package/dist/admin/components/OrderDetails.js +2 -1
  18. package/dist/admin/components/OrderDetails.js.map +1 -1
  19. package/dist/admin/components/ProductTable.d.ts.map +1 -1
  20. package/dist/admin/components/ProductTable.js +4 -3
  21. package/dist/admin/components/ProductTable.js.map +1 -1
  22. package/dist/admin/components/RecentOrders.d.ts.map +1 -1
  23. package/dist/admin/components/RecentOrders.js +2 -1
  24. package/dist/admin/components/RecentOrders.js.map +1 -1
  25. package/dist/admin/components/SEOPreview.d.ts +2 -8
  26. package/dist/admin/components/SEOPreview.d.ts.map +1 -1
  27. package/dist/admin/components/SEOPreview.js +32 -5
  28. package/dist/admin/components/SEOPreview.js.map +1 -1
  29. package/dist/admin/components/SecondaryMetricsRow.d.ts.map +1 -1
  30. package/dist/admin/components/SecondaryMetricsRow.js +3 -2
  31. package/dist/admin/components/SecondaryMetricsRow.js.map +1 -1
  32. package/dist/admin/components/StatsCards.d.ts.map +1 -1
  33. package/dist/admin/components/StatsCards.js +2 -1
  34. package/dist/admin/components/StatsCards.js.map +1 -1
  35. package/docs/changelog/README.md +53 -0
  36. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  37. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  38. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  39. package/docs/changelog/epoch-04-emails.md +65 -0
  40. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  41. package/docs/changelog/epoch-06-mobile-integrity.md +92 -0
  42. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  43. package/docs/design/admin-sidebar-and-band.html +542 -0
  44. package/package.json +3 -1
  45. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  46. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  47. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  48. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  49. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  50. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  51. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  52. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  53. package/templates/store-template/.env.example +50 -0
  54. package/templates/store-template/.mcp.json +8 -0
  55. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  56. package/templates/store-template/CLAUDE.md +14 -0
  57. package/templates/store-template/app/LayoutContent.tsx +106 -0
  58. package/templates/store-template/app/[...path]/page.tsx +79 -0
  59. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  60. package/templates/store-template/app/account/layout.tsx +17 -0
  61. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  62. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  63. package/templates/store-template/app/account/orders/page.tsx +153 -0
  64. package/templates/store-template/app/account/page.tsx +184 -0
  65. package/templates/store-template/app/account/profile/page.tsx +434 -0
  66. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  67. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  68. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  69. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  70. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  71. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  72. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  73. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  74. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  75. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  76. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  77. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  78. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  79. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  80. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  81. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  82. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  83. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  84. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  85. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  86. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  87. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  88. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  89. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  90. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  91. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  92. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  93. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  94. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  95. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  96. package/templates/store-template/app/admin/layout.tsx +47 -0
  97. package/templates/store-template/app/admin/login/page.tsx +528 -0
  98. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  99. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  100. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  101. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  102. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  103. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  104. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  105. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  106. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  107. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  108. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  109. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  110. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  111. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  112. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  113. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  115. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  116. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  117. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  118. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  119. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  120. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  121. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  122. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  124. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  125. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  126. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  127. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  128. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  129. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  130. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  131. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  132. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  133. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  134. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  135. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  136. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  137. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  138. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  139. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  140. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  141. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  142. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  143. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  144. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  145. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  146. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  147. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  148. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  149. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  150. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  151. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  152. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  153. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  154. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  155. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  156. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  157. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  158. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  159. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  160. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  161. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  162. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  163. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  164. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  165. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  166. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  167. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  168. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  169. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  170. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  171. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  172. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  173. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  174. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  175. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  176. package/templates/store-template/app/api/categories/route.ts +10 -0
  177. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  178. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  179. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  180. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  181. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  182. package/templates/store-template/app/api/checkout/route.ts +10 -0
  183. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  184. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  185. package/templates/store-template/app/api/contact/route.ts +164 -0
  186. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  187. package/templates/store-template/app/api/media/route.ts +7 -0
  188. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  189. package/templates/store-template/app/api/products/route.ts +10 -0
  190. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  191. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  192. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  193. package/templates/store-template/app/auth/layout.tsx +17 -0
  194. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  195. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  196. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  197. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  198. package/templates/store-template/app/cart/page.tsx +137 -0
  199. package/templates/store-template/app/checkout/page.tsx +88 -0
  200. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  201. package/templates/store-template/app/contact/layout.tsx +24 -0
  202. package/templates/store-template/app/contact/page.tsx +321 -0
  203. package/templates/store-template/app/faq/page.tsx +153 -0
  204. package/templates/store-template/app/globals.css +99 -0
  205. package/templates/store-template/app/layout.tsx +269 -0
  206. package/templates/store-template/app/not-found.tsx +35 -0
  207. package/templates/store-template/app/page.tsx +23 -0
  208. package/templates/store-template/app/privacy/page.tsx +281 -0
  209. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  210. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  211. package/templates/store-template/app/products/layout.tsx +26 -0
  212. package/templates/store-template/app/products/page.tsx +102 -0
  213. package/templates/store-template/app/robots.ts +25 -0
  214. package/templates/store-template/app/sitemap.ts +76 -0
  215. package/templates/store-template/app/terms/page.tsx +313 -0
  216. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  217. package/templates/store-template/components/Providers.tsx +37 -0
  218. package/templates/store-template/components/layout/Footer.tsx +150 -0
  219. package/templates/store-template/components/layout/Header.tsx +162 -0
  220. package/templates/store-template/components/sections/.gitkeep +7 -0
  221. package/templates/store-template/components/sections/CTA.tsx +51 -0
  222. package/templates/store-template/components/sections/Features.tsx +91 -0
  223. package/templates/store-template/components/sections/Hero.tsx +85 -0
  224. package/templates/store-template/eslint.config.mjs +18 -0
  225. package/templates/store-template/lib/store-config.ts +77 -0
  226. package/templates/store-template/lib/store-policy.ts +68 -0
  227. package/templates/store-template/lib/utils.ts +21 -0
  228. package/templates/store-template/next-env.d.ts +6 -0
  229. package/templates/store-template/next.config.js +105 -0
  230. package/templates/store-template/package.json +32 -0
  231. package/templates/store-template/postcss.config.js +6 -0
  232. package/templates/store-template/proxy.ts +150 -0
  233. package/templates/store-template/public/rovela-logo.png +0 -0
  234. package/templates/store-template/tailwind.config.ts +74 -0
  235. package/templates/store-template/tsconfig.json +41 -0
  236. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Layout Content Component
5
+ *
6
+ * Handles route-aware layout rendering to separate admin and customer experiences.
7
+ *
8
+ * - Customer routes (/products, /cart, etc.): Full store experience with Header, Footer, Cart
9
+ * - Admin routes (/admin/*): Minimal wrapper, admin layout handles its own chrome
10
+ *
11
+ * This separation ensures the store owner feels a clear disconnection between
12
+ * the admin panel and the customer-facing store.
13
+ */
14
+
15
+ import { usePathname } from 'next/navigation'
16
+ import { CartProvider, CartDrawer } from '@rovela-ai/sdk/cart'
17
+ import {
18
+ CookieConsentProvider,
19
+ type CookieConsentConfig,
20
+ CookieBanner,
21
+ } from '@rovela-ai/sdk/core/cookie-consent'
22
+ import { AnalyticsProvider } from '@rovela-ai/sdk/analytics'
23
+ import { RovelaLocaleProvider, type StoreLocale } from '@rovela-ai/sdk/core'
24
+ import { Header } from '@/components/layout/Header'
25
+ import { Footer } from '@/components/layout/Footer'
26
+ import { PathSyncBridge } from '@/components/PathSyncBridge'
27
+
28
+ interface LayoutContentProps {
29
+ children: React.ReactNode
30
+ /** R021: master toggle from store_settings.cookie_banner_enabled.
31
+ * When false, the banner never renders and the consent provider
32
+ * short-circuits (grants all categories). */
33
+ cookieBannerEnabled?: boolean
34
+ /** Merchant banner configuration (store_settings.cookie_consent, sanitized). */
35
+ cookieConsentConfig?: CookieConsentConfig
36
+ /** Region-rule verdict computed server-side from the visitor's IP country. */
37
+ consentRegionExempt?: boolean
38
+ /** Request-resolved storefront locale (root layout). */
39
+ locale?: StoreLocale
40
+ /** Server-resolved store locale config (hydration-safe navigation). */
41
+ defaultLocale?: StoreLocale
42
+ enabledLocales?: StoreLocale[]
43
+ }
44
+
45
+ export function LayoutContent({
46
+ children,
47
+ cookieBannerEnabled = true,
48
+ cookieConsentConfig,
49
+ consentRegionExempt = false,
50
+ locale = 'en',
51
+ defaultLocale,
52
+ enabledLocales,
53
+ }: LayoutContentProps) {
54
+ const pathname = usePathname()
55
+
56
+ // Check if current route is an admin route
57
+ const isAdminRoute = pathname?.startsWith('/admin')
58
+
59
+ // Admin routes: No store header/footer, no cart infrastructure.
60
+ // The admin layout (app/admin/(dashboard)/layout.tsx) provides its own chrome.
61
+ // Cookie banner is customer-facing only — admins don't see it either way.
62
+ //
63
+ // PathSyncBridge is mounted on BOTH branches so the Rovela editor's path
64
+ // bar reflects whichever URL the iframe is showing — admin or customer.
65
+ if (isAdminRoute) {
66
+ return (
67
+ <>
68
+ <PathSyncBridge />
69
+ {children}
70
+ </>
71
+ )
72
+ }
73
+
74
+ // Customer routes: Full store experience.
75
+ //
76
+ // R021: CookieConsentProvider wraps the storefront so any script (added
77
+ // by Claude Code in response to a merchant request) can gate itself via
78
+ // `useCookieConsent()`. CookieBanner is the EDPB-compliant UI; it only
79
+ // renders when the master toggle is on AND the user hasn't decided yet
80
+ // (or their 12-month consent expired). Do not remove — this is the
81
+ // GDPR compliance surface for EU/UK-seated stores.
82
+ // AnalyticsProvider (§46) is mounted INSIDE CookieConsentProvider so it can
83
+ // read consent state via useCookieConsent(). It auto-fires page_view +
84
+ // session_start on the customer branch only — admin sessions short-circuit
85
+ // automatically. Never mount on the admin branch.
86
+ return (
87
+ <RovelaLocaleProvider locale={locale} defaultLocale={defaultLocale} enabledLocales={enabledLocales}>
88
+ <CookieConsentProvider
89
+ enabled={cookieBannerEnabled}
90
+ config={cookieConsentConfig}
91
+ regionExempt={consentRegionExempt}
92
+ >
93
+ <AnalyticsProvider>
94
+ <CartProvider>
95
+ <PathSyncBridge />
96
+ <Header />
97
+ <main className="flex-1">{children}</main>
98
+ <Footer />
99
+ <CartDrawer />
100
+ <CookieBanner />
101
+ </CartProvider>
102
+ </AnalyticsProvider>
103
+ </CookieConsentProvider>
104
+ </RovelaLocaleProvider>
105
+ )
106
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Catch-all URL redirect — SEO migration bridge.
3
+ *
4
+ * Fires ONLY for paths no real route matched: old URLs from a previous platform
5
+ * (PrestaShop, etc.) plus genuine 404s. It reads the PrestaShop id out of the
6
+ * incoming old URL and permanently redirects to the matching new page, using the
7
+ * `store_url_map` the catalog import populates by design (id → new path). An
8
+ * optional `store_redirects` table (a merchant CSV export) gives exact-path
9
+ * overrides for URLs that carry no id. Anything unmapped renders the normal 404.
10
+ *
11
+ * Read-only and fully guarded: any DB / missing-table error degrades to a clean
12
+ * 404, so this can never break the storefront. `permanentRedirect` / `notFound`
13
+ * are called OUTSIDE the try/catch — they signal via a thrown control-flow error
14
+ * that must not be swallowed.
15
+ */
16
+ import { permanentRedirect, notFound } from 'next/navigation'
17
+ import { getSql } from '@rovela-ai/sdk/core/server'
18
+
19
+ export const dynamic = 'force-dynamic'
20
+
21
+ async function resolveRedirect(pathname: string): Promise<string | null> {
22
+ try {
23
+ const sql = getSql()
24
+
25
+ // 1. Exact-path overlay (optional merchant CSV export).
26
+ const exact = (await sql`
27
+ SELECT to_path FROM store_redirects WHERE from_path = ${pathname} LIMIT 1
28
+ `) as { to_path: string }[]
29
+ if (exact[0]?.to_path) return exact[0].to_path
30
+
31
+ // 2. By-design id engine — read the id out of the old URL (config-independent).
32
+ const segments = pathname.split('/').filter(Boolean)
33
+ if (segments.length === 0) return null
34
+ const idMatch = segments[segments.length - 1].match(/^(\d+)-/)
35
+ if (!idMatch) return null
36
+ const psId = idMatch[1]
37
+
38
+ // PrestaShop shape: products end in .html and/or sit under a category path;
39
+ // categories are a single `/{id}-{slug}` segment.
40
+ const kind =
41
+ pathname.endsWith('.html') || segments.length >= 2 ? 'product' : 'category'
42
+
43
+ // PrestaShop root/home categories (ids 1, 2) → homepage.
44
+ if (kind === 'category' && (psId === '1' || psId === '2')) return '/'
45
+
46
+ const hit = (await sql`
47
+ SELECT to_path FROM store_url_map WHERE kind = ${kind} AND ps_id = ${psId} LIMIT 1
48
+ `) as { to_path: string }[]
49
+ if (hit[0]?.to_path) return hit[0].to_path
50
+
51
+ // 3. Fallback for an unmapped (inactive/deleted) product: if the old URL's
52
+ // leading category segment resolves to a live category, send them there
53
+ // instead of 404. Never blanket-redirect to homepage (that reads as a
54
+ // soft 404 to search engines).
55
+ if (kind === 'product' && segments.length >= 2) {
56
+ const cat = (await sql`
57
+ SELECT id FROM categories WHERE slug = ${segments[0]} LIMIT 1
58
+ `) as { id: string }[]
59
+ if (cat[0]?.id) return `/products?category=${cat[0].id}`
60
+ }
61
+
62
+ return null
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ export default async function CatchAllRedirect({
69
+ params,
70
+ }: {
71
+ params: Promise<{ path?: string[] }>
72
+ }) {
73
+ const { path = [] } = await params
74
+ const pathname = '/' + path.map((s) => decodeURIComponent(s)).join('/')
75
+
76
+ const to = await resolveRedirect(pathname)
77
+ if (to) permanentRedirect(to)
78
+ notFound()
79
+ }
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Customer Addresses Page
5
+ *
6
+ * Self-service address book (add / edit / remove / set default). Admin identity
7
+ * cannot reach this page: `useAuth()` role-filters the session and `AuthGuard`
8
+ * redirects anything that isn't a signed-in customer to `/auth/signin`.
9
+ */
10
+
11
+ import { LocaleLink as Link } from '@rovela-ai/sdk/core'
12
+ import { ChevronLeft } from 'lucide-react'
13
+ import { AuthGuard } from '@rovela-ai/sdk/auth'
14
+ import { AddressBook } from '@rovela-ai/sdk/auth'
15
+ import { useT } from '@rovela-ai/sdk/core'
16
+
17
+ function AddressesContent() {
18
+ const t = useT()
19
+ return (
20
+ <div className="container mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
21
+ <Link
22
+ href="/account"
23
+ className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
24
+ >
25
+ <ChevronLeft className="h-4 w-4" />
26
+ {t('account.backToAccountLower')}
27
+ </Link>
28
+
29
+ <h1 className="mb-2 text-3xl font-bold font-heading">{t('account.myAddresses')}</h1>
30
+ <p className="mb-8 text-sm text-muted-foreground">
31
+ {t('account.addressesHint')}
32
+ </p>
33
+
34
+ <AddressBook />
35
+ </div>
36
+ )
37
+ }
38
+
39
+ export default function AddressesPage() {
40
+ return (
41
+ <AuthGuard redirectTo="/auth/signin">
42
+ <AddressesContent />
43
+ </AuthGuard>
44
+ )
45
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Account area gate — when the merchant sets Customer accounts to Off
3
+ * (admin Settings → Checkout Settings), the whole /account area redirects
4
+ * home BEFORE any HTML ships. Customer data is never deleted — flipping
5
+ * the setting back restores access. Fail-open on a settings-read error.
6
+ */
7
+
8
+ import { redirect } from 'next/navigation'
9
+ import { findSettings } from '@rovela-ai/sdk/core/server'
10
+
11
+ export const dynamic = 'force-dynamic'
12
+
13
+ export default async function AccountLayout({ children }: { children: React.ReactNode }) {
14
+ const settings = await findSettings().catch(() => null)
15
+ if (settings?.customerAccounts === false) redirect('/')
16
+ return <>{children}</>
17
+ }
@@ -0,0 +1,277 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Order Invoice Page
5
+ *
6
+ * A clean, print-ready invoice for a single order, built from the immutable
7
+ * order snapshot + store settings. "Download" uses the browser's print-to-PDF.
8
+ * Admin identity cannot reach this page (AuthGuard + role-filtered useAuth).
9
+ */
10
+
11
+ import { useState, useEffect, use } from 'react'
12
+ import { LocaleLink as Link } from '@rovela-ai/sdk/core'
13
+ import { ChevronLeft, Printer, Loader2 } from 'lucide-react'
14
+ import { AuthGuard } from '@rovela-ai/sdk/auth'
15
+ import { useStoreSettings, useT, useStoreFormatting } from '@rovela-ai/sdk/core'
16
+
17
+ interface InvoiceItem {
18
+ id: string
19
+ name: string
20
+ quantity: number
21
+ price: number
22
+ attributes?: Record<string, string>
23
+ }
24
+
25
+ interface InvoiceAddress {
26
+ firstName?: string
27
+ lastName?: string
28
+ name?: string
29
+ company?: string
30
+ line1: string
31
+ line2?: string
32
+ city: string
33
+ state?: string
34
+ postalCode: string
35
+ country: string
36
+ phone?: string
37
+ }
38
+
39
+ interface InvoiceOrder {
40
+ id: string
41
+ status: string
42
+ total: number
43
+ subtotal: number
44
+ tax: number
45
+ shipping: number
46
+ discountAmount?: number | string
47
+ discountCode?: string | null
48
+ refundAmount?: number
49
+ createdAt: string
50
+ items: InvoiceItem[]
51
+ shippingAddress: InvoiceAddress
52
+ billingAddress?: InvoiceAddress
53
+ customerEmail?: string
54
+ }
55
+
56
+ function addrName(a: InvoiceAddress): string {
57
+ if (a.firstName || a.lastName) return `${a.firstName ?? ''} ${a.lastName ?? ''}`.trim()
58
+ return a.name ?? ''
59
+ }
60
+
61
+ function AddressBlock({ address }: { address: InvoiceAddress }) {
62
+ return (
63
+ <div className="text-sm text-muted-foreground leading-relaxed">
64
+ <p className="font-medium text-foreground">{addrName(address)}</p>
65
+ {address.company && <p>{address.company}</p>}
66
+ <p>{address.line1}</p>
67
+ {address.line2 && <p>{address.line2}</p>}
68
+ <p>
69
+ {address.city}
70
+ {address.state ? `, ${address.state}` : ''} {address.postalCode}
71
+ </p>
72
+ <p>{address.country}</p>
73
+ {address.phone && <p>{address.phone}</p>}
74
+ </div>
75
+ )
76
+ }
77
+
78
+ function InvoiceContent({ orderId }: { orderId: string }) {
79
+ const t = useT()
80
+ const { settings } = useStoreSettings()
81
+ // Locale-aware price and date shapes (active i18n locale + store currency).
82
+ const { formatPrice, formatDate } = useStoreFormatting()
83
+ const [order, setOrder] = useState<InvoiceOrder | null>(null)
84
+ // Per-order snapshot (orders.currency): the order renders in the currency
85
+ // it was PLACED in, even after the store's currency changes.
86
+ const orderCurrency = (order as { currency?: string | null } | null)?.currency
87
+ const [isLoading, setIsLoading] = useState(true)
88
+ const [error, setError] = useState<string | null>(null)
89
+
90
+ useEffect(() => {
91
+ let active = true
92
+ async function load() {
93
+ try {
94
+ const res = await fetch(`/api/account/orders/${orderId}`)
95
+ if (!res.ok) {
96
+ if (active) setError(res.status === 404 ? t('account.orderNotFound') : t('account.orderLoadFailed'))
97
+ return
98
+ }
99
+ const data = await res.json()
100
+ if (active) setOrder(data.order ?? data)
101
+ } catch {
102
+ if (active) setError(t('account.orderLoadFailed'))
103
+ } finally {
104
+ if (active) setIsLoading(false)
105
+ }
106
+ }
107
+ load()
108
+ return () => {
109
+ active = false
110
+ }
111
+ }, [orderId])
112
+
113
+ if (isLoading) {
114
+ return (
115
+ <div className="flex items-center justify-center py-24 text-muted-foreground">
116
+ <Loader2 className="h-6 w-6 animate-spin" />
117
+ </div>
118
+ )
119
+ }
120
+
121
+ if (error || !order) {
122
+ return (
123
+ <div className="container mx-auto max-w-2xl px-4 py-16 text-center">
124
+ <p className="text-muted-foreground">{error || t('account.orderNotFound')}</p>
125
+ <Link href="/account/orders" className="mt-4 inline-block text-sm text-primary hover:underline">
126
+ {t('invoice.backToOrders')}
127
+ </Link>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ const billing = order.billingAddress ?? order.shippingAddress
133
+
134
+ return (
135
+ <div className="container mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
136
+ {/* Screen-only controls */}
137
+ <div className="mb-6 flex items-center justify-between print:hidden">
138
+ <Link
139
+ href={`/account/orders/${order.id}`}
140
+ className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
141
+ >
142
+ <ChevronLeft className="h-4 w-4" />
143
+ {t('invoice.backToOrder')}
144
+ </Link>
145
+ <button
146
+ type="button"
147
+ onClick={() => window.print()}
148
+ className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
149
+ >
150
+ <Printer className="h-4 w-4" />
151
+ {t('invoice.printSavePdf')}
152
+ </button>
153
+ </div>
154
+
155
+ {/* Invoice document */}
156
+ <div className="rounded-lg border bg-card p-8 print:border-0 print:p-0">
157
+ {/* Header */}
158
+ <div className="flex flex-col gap-6 border-b pb-6 sm:flex-row sm:items-start sm:justify-between">
159
+ <div>
160
+ {settings?.logoUrl ? (
161
+ // eslint-disable-next-line @next/next/no-img-element
162
+ <img src={settings.logoUrl} alt={settings.storeName} className="mb-2 h-10 w-auto object-contain" />
163
+ ) : (
164
+ <p className="text-xl font-bold font-heading">{settings?.storeName}</p>
165
+ )}
166
+ {settings?.storeEmail && (
167
+ <p className="text-sm text-muted-foreground">{settings.storeEmail}</p>
168
+ )}
169
+ </div>
170
+ <div className="text-left sm:text-right">
171
+ <h1 className="text-2xl font-bold font-heading">{t('invoice.title')}</h1>
172
+ <p className="mt-1 text-sm text-muted-foreground">
173
+ #{order.id.slice(0, 8).toUpperCase()}
174
+ </p>
175
+ <p className="text-sm text-muted-foreground">
176
+ {formatDate(new Date(order.createdAt))}
177
+ </p>
178
+ </div>
179
+ </div>
180
+
181
+ {/* Parties */}
182
+ <div className="grid grid-cols-1 gap-8 py-6 sm:grid-cols-2">
183
+ <div>
184
+ <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
185
+ {t('invoice.billTo')}
186
+ </p>
187
+ <AddressBlock address={billing} />
188
+ </div>
189
+ <div>
190
+ <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
191
+ {t('invoice.shipTo')}
192
+ </p>
193
+ <AddressBlock address={order.shippingAddress} />
194
+ </div>
195
+ </div>
196
+
197
+ {/* Line items */}
198
+ <table className="w-full text-sm">
199
+ <thead>
200
+ <tr className="border-b text-left text-xs uppercase tracking-wide text-muted-foreground">
201
+ <th className="py-2 font-semibold">{t('invoice.item')}</th>
202
+ <th className="py-2 text-center font-semibold">{t('invoice.qty')}</th>
203
+ <th className="py-2 text-right font-semibold">{t('invoice.price')}</th>
204
+ <th className="py-2 text-right font-semibold">{t('common.total')}</th>
205
+ </tr>
206
+ </thead>
207
+ <tbody>
208
+ {order.items.map((item, i) => (
209
+ <tr key={item.id || i} className="border-b border-border/60">
210
+ <td className="py-3">
211
+ <p className="font-medium text-foreground">{item.name}</p>
212
+ {item.attributes && Object.keys(item.attributes).length > 0 && (
213
+ <p className="text-xs text-muted-foreground">
214
+ {Object.entries(item.attributes)
215
+ .map(([k, v]) => `${k}: ${v}`)
216
+ .join(', ')}
217
+ </p>
218
+ )}
219
+ </td>
220
+ <td className="py-3 text-center text-muted-foreground">{item.quantity}</td>
221
+ <td className="py-3 text-right text-muted-foreground">{formatPrice(item.price, orderCurrency)}</td>
222
+ <td className="py-3 text-right font-medium">
223
+ {formatPrice(item.price * item.quantity, orderCurrency)}
224
+ </td>
225
+ </tr>
226
+ ))}
227
+ </tbody>
228
+ </table>
229
+
230
+ {/* Totals */}
231
+ <div className="ml-auto mt-6 w-full max-w-xs space-y-2 text-sm">
232
+ <div className="flex justify-between text-muted-foreground">
233
+ <span>{t('common.subtotal')}</span>
234
+ <span>{formatPrice(order.subtotal, orderCurrency)}</span>
235
+ </div>
236
+ {Number(order.discountAmount ?? 0) > 0 && (
237
+ <div className="flex justify-between text-emerald-600">
238
+ <span>{t('common.discount')}{order.discountCode ? ` (${order.discountCode})` : ''}</span>
239
+ <span>-{formatPrice(order.discountAmount!, orderCurrency)}</span>
240
+ </div>
241
+ )}
242
+ <div className="flex justify-between text-muted-foreground">
243
+ <span>{t('common.shipping')}</span>
244
+ <span>{order.shipping > 0 ? formatPrice(order.shipping, orderCurrency) : t('common.free')}</span>
245
+ </div>
246
+ <div className="flex justify-between text-muted-foreground">
247
+ <span>{t('common.tax')}</span>
248
+ <span>{formatPrice(order.tax, orderCurrency)}</span>
249
+ </div>
250
+ <div className="flex justify-between border-t pt-2 text-base font-semibold text-foreground">
251
+ <span>{t('common.total')}</span>
252
+ <span>{formatPrice(order.total, orderCurrency)}</span>
253
+ </div>
254
+ {order.refundAmount != null && order.refundAmount > 0 && (
255
+ <div className="flex justify-between text-amber-600">
256
+ <span>{t('account.refunded')}</span>
257
+ <span>-{formatPrice(order.refundAmount)}</span>
258
+ </div>
259
+ )}
260
+ </div>
261
+
262
+ <p className="mt-8 border-t pt-6 text-center text-xs text-muted-foreground">
263
+ {t('invoice.thankYou')}
264
+ </p>
265
+ </div>
266
+ </div>
267
+ )
268
+ }
269
+
270
+ export default function InvoicePage({ params }: { params: Promise<{ id: string }> }) {
271
+ const { id } = use(params)
272
+ return (
273
+ <AuthGuard redirectTo="/auth/signin">
274
+ <InvoiceContent orderId={id} />
275
+ </AuthGuard>
276
+ )
277
+ }