@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,281 @@
1
+ import type { Metadata } from 'next'
2
+ import { LocaleLink as Link } from '@rovela-ai/sdk/core'
3
+ import { getBlueprint, localizedAlternates, findSettings } from '@rovela-ai/sdk/core/server'
4
+
5
+ /**
6
+ * Privacy Policy Page
7
+ *
8
+ * Static privacy policy page with standard e-commerce sections.
9
+ * Store name is pulled from blueprint for personalization.
10
+ */
11
+
12
+ // =============================================================================
13
+ // Metadata
14
+ // =============================================================================
15
+
16
+ export async function generateMetadata(): Promise<Metadata> {
17
+ let storeName = 'Our Store'
18
+ try {
19
+ const blueprint = getBlueprint()
20
+ storeName = blueprint?.store?.name || process.env.STORE_NAME || storeName
21
+ } catch {
22
+ // Blueprint might not exist
23
+ }
24
+
25
+ return {
26
+ title: 'Privacy Policy',
27
+ description: `Privacy Policy for ${storeName}. Learn how we collect, use, and protect your personal information.`,
28
+ alternates: await localizedAlternates('/privacy'),
29
+ }
30
+ }
31
+
32
+ // =============================================================================
33
+ // Page Component
34
+ // =============================================================================
35
+
36
+ export default async function PrivacyPage() {
37
+ // Get store name for display — LIVE settings first (store_settings is what
38
+ // the merchant edits in /admin; blueprint + env are build-time fallbacks
39
+ // that go stale after a rename). Fail-open on DB errors.
40
+ let storeName = 'Our Store'
41
+ let supportEmail = ''
42
+ const liveSettings = await findSettings().catch(() => null)
43
+
44
+ try {
45
+ const blueprint = getBlueprint()
46
+ storeName =
47
+ liveSettings?.storeName || blueprint?.store?.name || process.env.STORE_NAME || storeName
48
+ } catch {
49
+ storeName = liveSettings?.storeName || process.env.STORE_NAME || storeName
50
+ }
51
+
52
+ // Public contact email = the merchant's own address only. NEVER the platform
53
+ // transactional sender (RESEND_FROM_EMAIL): surfacing that put
54
+ // "contact@rovela.ai" on storefront legal pages and read as a template
55
+ // artifact. Falls back to support@<your-domain> derived from the live host;
56
+ // if only a *.rovela.app / *.vercel.app / localhost host is available it stays
57
+ // empty and the email line is hidden (the contact-page link still renders).
58
+ const storeHost = (process.env.NEXT_PUBLIC_APP_URL || '')
59
+ .replace(/^https?:\/\//, '')
60
+ .replace(/\/.*$/, '')
61
+ .trim()
62
+ const hasOwnDomain =
63
+ !!storeHost &&
64
+ !storeHost.endsWith('rovela.app') &&
65
+ !storeHost.endsWith('vercel.app') &&
66
+ !storeHost.startsWith('localhost')
67
+ // Live store email first — this is the address the merchant actually set
68
+ // (emails, contact page and JSON-LD already read it); SUPPORT_EMAIL env is
69
+ // the build-time fallback, support@<domain> the last resort.
70
+ supportEmail =
71
+ liveSettings?.storeEmail?.trim() ||
72
+ process.env.SUPPORT_EMAIL?.trim() ||
73
+ (hasOwnDomain ? `support@${storeHost}` : '')
74
+
75
+ return (
76
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
77
+ <div className="max-w-3xl mx-auto">
78
+ {/* Header */}
79
+ <div className="mb-12">
80
+ <h1 className="text-3xl font-bold font-heading">Privacy Policy</h1>
81
+ </div>
82
+
83
+ {/* Content */}
84
+ <div className="prose prose-neutral dark:prose-invert max-w-none">
85
+ {/* Introduction */}
86
+ <section className="mb-10">
87
+ <p className="text-muted-foreground leading-relaxed">
88
+ At {storeName}, we are committed to protecting your privacy and ensuring the
89
+ security of your personal information. This Privacy Policy explains how we collect,
90
+ use, disclose, and safeguard your information when you visit our website or make
91
+ a purchase.
92
+ </p>
93
+ </section>
94
+
95
+ {/* Information We Collect */}
96
+ <section className="mb-10">
97
+ <h2 className="text-xl font-semibold font-heading mb-4">
98
+ Information We Collect
99
+ </h2>
100
+ <p className="text-muted-foreground mb-4">
101
+ We collect information that you provide directly to us, including:
102
+ </p>
103
+ <ul className="list-disc pl-6 space-y-2 text-muted-foreground">
104
+ <li>Name and contact information (email address, phone number, shipping address)</li>
105
+ <li>Payment information (processed securely through our payment provider)</li>
106
+ <li>Order history and preferences</li>
107
+ <li>Communications you send to us</li>
108
+ <li>Account information if you create an account</li>
109
+ </ul>
110
+ <p className="text-muted-foreground mt-4">
111
+ We also automatically collect certain information when you visit our website,
112
+ including your IP address, browser type, device information, and browsing behavior.
113
+ </p>
114
+ </section>
115
+
116
+ {/* How We Use Your Information */}
117
+ <section className="mb-10">
118
+ <h2 className="text-xl font-semibold font-heading mb-4">
119
+ How We Use Your Information
120
+ </h2>
121
+ <p className="text-muted-foreground mb-4">
122
+ We use the information we collect to:
123
+ </p>
124
+ <ul className="list-disc pl-6 space-y-2 text-muted-foreground">
125
+ <li>Process and fulfill your orders</li>
126
+ <li>Send you order confirmations and shipping updates</li>
127
+ <li>Respond to your questions and provide customer support</li>
128
+ <li>Send promotional communications (with your consent)</li>
129
+ <li>Improve our website and services</li>
130
+ <li>Detect and prevent fraud</li>
131
+ <li>Comply with legal obligations</li>
132
+ </ul>
133
+ </section>
134
+
135
+ {/* Information Sharing */}
136
+ <section className="mb-10">
137
+ <h2 className="text-xl font-semibold font-heading mb-4">
138
+ Information Sharing
139
+ </h2>
140
+ <p className="text-muted-foreground mb-4">
141
+ We do not sell your personal information. We may share your information with:
142
+ </p>
143
+ <ul className="list-disc pl-6 space-y-2 text-muted-foreground">
144
+ <li>Service providers who assist in our operations (payment processors, shipping carriers)</li>
145
+ <li>Analytics providers to help us understand website usage</li>
146
+ <li>Legal authorities when required by law</li>
147
+ </ul>
148
+ </section>
149
+
150
+ {/* Data Security */}
151
+ <section className="mb-10">
152
+ <h2 className="text-xl font-semibold font-heading mb-4">
153
+ Data Security
154
+ </h2>
155
+ <p className="text-muted-foreground">
156
+ We implement appropriate technical and organizational measures to protect your
157
+ personal information against unauthorized access, alteration, disclosure, or
158
+ destruction. All payment transactions are encrypted using SSL technology and
159
+ processed through secure payment providers.
160
+ </p>
161
+ </section>
162
+
163
+ {/* Cookies */}
164
+ <section id="cookies" className="mb-10 scroll-mt-24">
165
+ <h2 className="text-xl font-semibold font-heading mb-4">
166
+ Cookies and Tracking
167
+ </h2>
168
+ <p className="text-muted-foreground mb-4">
169
+ We use cookies and similar technologies to make this store work
170
+ and, with your consent, to understand how it is used and show
171
+ you relevant content. Cookies are grouped into three categories:
172
+ </p>
173
+ <ul className="list-disc pl-6 space-y-2 text-muted-foreground">
174
+ <li>
175
+ <strong className="text-foreground">Essential.</strong> Required
176
+ for the store to work — including your cart, session, and
177
+ sign-in state. Always on; cannot be disabled.
178
+ </li>
179
+ <li>
180
+ <strong className="text-foreground">Analytics.</strong> Help us
181
+ understand how the store is used so we can improve it.
182
+ Optional; off unless you choose to allow them.
183
+ </li>
184
+ <li>
185
+ <strong className="text-foreground">Marketing.</strong> Used to
186
+ measure ads and show you relevant content across sites.
187
+ Optional; off unless you choose to allow them.
188
+ </li>
189
+ </ul>
190
+ <p className="text-muted-foreground mt-4">
191
+ You can change your preferences at any time by clicking
192
+ &ldquo;Cookie preferences&rdquo; in the footer of any page. If
193
+ you reject non-essential cookies, some features that rely on
194
+ analytics or personalization may be limited.
195
+ </p>
196
+ </section>
197
+
198
+ {/* Your Rights */}
199
+ <section className="mb-10">
200
+ <h2 className="text-xl font-semibold font-heading mb-4">
201
+ Your Rights
202
+ </h2>
203
+ <p className="text-muted-foreground mb-4">
204
+ Depending on your location, you may have the right to:
205
+ </p>
206
+ <ul className="list-disc pl-6 space-y-2 text-muted-foreground">
207
+ <li>Access the personal information we hold about you</li>
208
+ <li>Request correction of inaccurate information</li>
209
+ <li>Request deletion of your personal information</li>
210
+ <li>Opt out of marketing communications</li>
211
+ <li>Data portability</li>
212
+ </ul>
213
+ </section>
214
+
215
+ {/* Children's Privacy */}
216
+ <section className="mb-10">
217
+ <h2 className="text-xl font-semibold font-heading mb-4">
218
+ Children&apos;s Privacy
219
+ </h2>
220
+ <p className="text-muted-foreground">
221
+ Our website is not intended for children under 13 years of age. We do not
222
+ knowingly collect personal information from children. If you believe we have
223
+ collected information from a child, please contact us immediately.
224
+ </p>
225
+ </section>
226
+
227
+ {/* Changes to Policy */}
228
+ <section className="mb-10">
229
+ <h2 className="text-xl font-semibold font-heading mb-4">
230
+ Changes to This Policy
231
+ </h2>
232
+ <p className="text-muted-foreground">
233
+ We may update this Privacy Policy from time to time. We will notify you of any
234
+ changes by posting the new policy on this page and updating the &quot;Last updated&quot;
235
+ date. We encourage you to review this policy periodically.
236
+ </p>
237
+ </section>
238
+
239
+ {/* Contact Us */}
240
+ <section className="mb-10">
241
+ <h2 className="text-xl font-semibold font-heading mb-4">
242
+ Contact Us
243
+ </h2>
244
+ <p className="text-muted-foreground">
245
+ If you have any questions about this Privacy Policy or our privacy
246
+ practices, please
247
+ {supportEmail ? (
248
+ <>
249
+ {' '}contact us at{' '}
250
+ <a
251
+ href={`mailto:${supportEmail}`}
252
+ className="text-primary hover:underline"
253
+ >
254
+ {supportEmail}
255
+ </a>{' '}
256
+ or visit our{' '}
257
+ </>
258
+ ) : (
259
+ <>{' '}reach us through our{' '}</>
260
+ )}
261
+ <Link href="/contact" className="text-primary hover:underline">
262
+ contact page
263
+ </Link>
264
+ .
265
+ </p>
266
+ </section>
267
+ </div>
268
+
269
+ {/* Back to Home */}
270
+ <div className="mt-12 pt-8 border-t">
271
+ <Link
272
+ href="/"
273
+ className="text-sm text-primary hover:underline"
274
+ >
275
+ &larr; Back to Home
276
+ </Link>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ )
281
+ }
@@ -0,0 +1,179 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Product Detail — interactive client island.
5
+ *
6
+ * Rendered by the server `page.tsx`, which owns metadata + the server-rendered
7
+ * Product JSON-LD (AI shopping crawlers don't run JS, so schema must be server
8
+ * side). This component keeps the original interactive UX unchanged:
9
+ * - ProductGallery: Image gallery
10
+ * - VariantSelector: Variant selection
11
+ * - AddToCartButton: Add to cart with feedback
12
+ * - useProduct: Single product data fetching
13
+ */
14
+
15
+ import { useState, useMemo } from 'react'
16
+ import { notFound } from 'next/navigation'
17
+ import { LocaleLink as Link } from '@rovela-ai/sdk/core'
18
+ import { ChevronLeft } from 'lucide-react'
19
+ import {
20
+ ProductGallery,
21
+ ProductSpecifications,
22
+ VariantSelector,
23
+ useProduct,
24
+ } from '@rovela-ai/sdk/products'
25
+ import { AddToCartButton } from '@rovela-ai/sdk/cart'
26
+ import { useT, useStoreFormatting } from '@rovela-ai/sdk/core'
27
+ import type { ProductVariant } from '@rovela-ai/sdk/core'
28
+
29
+ export function ProductDetailClient({ slug }: { slug: string }) {
30
+ const { product, variants, isLoading, error } = useProduct(slug)
31
+ const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null)
32
+ const t = useT()
33
+ // Locale-aware price shapes (active i18n locale + store currency).
34
+ const { formatPrice } = useStoreFormatting()
35
+
36
+ // ALL HOOKS AND COMPUTED VALUES MUST BE ABOVE EARLY RETURNS (Rules of Hooks)
37
+
38
+ // Get current variant or default to first (safe even if variants is empty)
39
+ const currentVariant = selectedVariant || (variants.length > 0 ? variants[0] : null)
40
+
41
+ // Compute gallery: variant image + variant video first (when set), then
42
+ // product images, then product videos. Filter duplicates so a variant
43
+ // URL that also appears in product arrays only renders once.
44
+ const galleryMedia = useMemo<string[]>(() => {
45
+ if (!product) return []
46
+ const variantUrls = [currentVariant?.image, currentVariant?.video].filter(
47
+ (u): u is string => !!u
48
+ )
49
+ const productImages = product.images || []
50
+ const productVideos = product.videos || []
51
+ if (variantUrls.length === 0) {
52
+ return [...productImages, ...productVideos]
53
+ }
54
+ const variantSet = new Set(variantUrls)
55
+ return [
56
+ ...variantUrls,
57
+ ...productImages.filter((u) => !variantSet.has(u)),
58
+ ...productVideos.filter((u) => !variantSet.has(u)),
59
+ ]
60
+ }, [product, currentVariant])
61
+
62
+ // Generate a key to reset gallery when variant changes
63
+ const galleryKey = currentVariant?.id || 'no-variant'
64
+
65
+ // Determine price and stock (safe if product is null)
66
+ const price = currentVariant?.price ?? product?.price ?? 0
67
+ const comparePrice = product?.comparePrice
68
+ const inStock = currentVariant
69
+ ? (currentVariant.inventory ?? 0) > 0
70
+ : true
71
+
72
+ // EARLY RETURNS AFTER ALL HOOKS
73
+
74
+ // Loading state
75
+ if (isLoading) {
76
+ return (
77
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
78
+ <div className="animate-pulse">
79
+ <div className="h-8 w-48 bg-muted rounded mb-8" />
80
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
81
+ <div className="aspect-square bg-muted rounded-lg" />
82
+ <div className="space-y-4">
83
+ <div className="h-10 w-3/4 bg-muted rounded" />
84
+ <div className="h-6 w-1/4 bg-muted rounded" />
85
+ <div className="h-24 bg-muted rounded" />
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ // Error or not found
94
+ if (error || !product) {
95
+ notFound()
96
+ }
97
+
98
+ return (
99
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
100
+ {/* Breadcrumb */}
101
+ <nav className="mb-8">
102
+ <Link
103
+ href="/products"
104
+ className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
105
+ >
106
+ <ChevronLeft className="h-4 w-4 mr-1" />
107
+ {t('listing.backToProducts')}
108
+ </Link>
109
+ </nav>
110
+
111
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
112
+ {/* Product Gallery */}
113
+ <ProductGallery
114
+ key={galleryKey}
115
+ media={galleryMedia}
116
+ alt={product.name}
117
+ />
118
+
119
+ {/* Product Info */}
120
+ <div className="flex flex-col">
121
+ <h1 className="text-3xl font-bold font-heading">{product.name}</h1>
122
+
123
+ {/* Price */}
124
+ <div className="mt-4 flex items-baseline gap-3">
125
+ <span className="text-2xl font-semibold">
126
+ {formatPrice(price)}
127
+ </span>
128
+ {comparePrice && comparePrice > price && (
129
+ <span className="text-lg text-muted-foreground line-through">
130
+ {formatPrice(comparePrice)}
131
+ </span>
132
+ )}
133
+ </div>
134
+
135
+ {/* Description */}
136
+ {product.description && (
137
+ <p className="mt-6 whitespace-pre-line text-muted-foreground">
138
+ {product.description}
139
+ </p>
140
+ )}
141
+
142
+ {/* Variant Selector */}
143
+ {variants && variants.length > 1 && (
144
+ <div className="mt-8">
145
+ <VariantSelector
146
+ variants={variants}
147
+ selected={currentVariant}
148
+ onSelect={setSelectedVariant}
149
+ />
150
+ </div>
151
+ )}
152
+
153
+ {/* Add to Cart */}
154
+ <div className="mt-8">
155
+ <AddToCartButton
156
+ product={product}
157
+ selectedVariant={currentVariant}
158
+ disabled={!inStock}
159
+ showFeedback
160
+ className="w-full sm:w-auto"
161
+ >
162
+ {inStock ? t('cart.addToCart') : t('product.outOfStock')}
163
+ </AddToCartButton>
164
+ </div>
165
+
166
+ {/* Stock Status */}
167
+ {!inStock && (
168
+ <p className="mt-4 text-sm text-destructive">
169
+ {t('product.currentlyOutOfStock')}
170
+ </p>
171
+ )}
172
+ </div>
173
+ </div>
174
+
175
+ {/* Specifications (custom fields) — renders nothing when empty */}
176
+ <ProductSpecifications metafields={product.metafields} className="mt-12 max-w-3xl" />
177
+ </div>
178
+ )
179
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Product Detail Page — server shell.
3
+ *
4
+ * This is a Server Component so it can emit per-product metadata AND a
5
+ * server-rendered `Product` + `Offer` JSON-LD block. AI shopping crawlers
6
+ * (GPTBot, OAI-SearchBot, PerplexityBot) read structured data BEFORE HTML and
7
+ * do not execute JavaScript, so the schema must be server-rendered here — not
8
+ * injected by the client island. The interactive UI lives in
9
+ * `ProductDetailClient` and is unchanged.
10
+ */
11
+
12
+ import type { Metadata } from 'next'
13
+ import { cache } from 'react'
14
+ import { notFound } from 'next/navigation'
15
+ import {
16
+ findProductBySlug,
17
+ findProductVariants,
18
+ findSettings,
19
+ localizedAlternates,
20
+ getRequestLocale,
21
+ } from '@rovela-ai/sdk/core/server'
22
+ import { localizeCatalogRow } from '@rovela-ai/sdk/core'
23
+ import { buildProductJsonLd, buildBreadcrumbJsonLd, serializeJsonLd } from '@rovela-ai/sdk/core'
24
+ import { ProductDetailClient } from './ProductDetailClient'
25
+
26
+ const STORE_URL = process.env.NEXT_PUBLIC_APP_URL || ''
27
+
28
+ // Dedupe the product lookup across generateMetadata + the page render within a request.
29
+ const getProduct = cache((slug: string) =>
30
+ findProductBySlug(slug).catch(() => null)
31
+ )
32
+
33
+ function toPlainText(html: string): string {
34
+ return html
35
+ .replace(/<[^>]*>/g, ' ')
36
+ .replace(/\s+/g, ' ')
37
+ .trim()
38
+ }
39
+
40
+ type Props = { params: Promise<{ slug: string }> }
41
+
42
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
43
+ const { slug } = await params
44
+ const fetchedProduct = await getProduct(slug)
45
+ const { locale: activeLocale, config: localeConfig } = await getRequestLocale()
46
+ // Shadow with the localized copy so every downstream usage (title, JSON-LD,
47
+ // breadcrumb, client island props) inherits the translation.
48
+ const product = fetchedProduct
49
+ ? localizeCatalogRow(fetchedProduct, activeLocale, localeConfig.defaultLocale)
50
+ : fetchedProduct
51
+ if (!product) return { title: 'Product not found' }
52
+
53
+ const title = product.metaTitle || product.name
54
+ const description =
55
+ product.metaDescription ||
56
+ (product.description ? toPlainText(product.description).slice(0, 160) : undefined)
57
+ const image = product.images?.[0]
58
+
59
+ return {
60
+ title,
61
+ ...(description && { description }),
62
+ alternates: await localizedAlternates(`/products/${slug}`),
63
+ ...(product.metaKeywords && product.metaKeywords.length > 0 && {
64
+ keywords: product.metaKeywords,
65
+ }),
66
+ openGraph: {
67
+ title,
68
+ ...(description && { description }),
69
+ type: 'website',
70
+ url: `/products/${slug}`,
71
+ ...(image && { images: [{ url: image }] }),
72
+ },
73
+ twitter: {
74
+ card: 'summary_large_image',
75
+ title,
76
+ ...(description && { description }),
77
+ ...(image && { images: [image] }),
78
+ },
79
+ }
80
+ }
81
+
82
+ export default async function ProductPage({ params }: Props) {
83
+ const { slug } = await params
84
+ const fetchedProduct = await getProduct(slug)
85
+ const { locale: activeLocale, config: localeConfig } = await getRequestLocale()
86
+ // Shadow with the localized copy so every downstream usage (title, JSON-LD,
87
+ // breadcrumb, client island props) inherits the translation.
88
+ const product = fetchedProduct
89
+ ? localizeCatalogRow(fetchedProduct, activeLocale, localeConfig.defaultLocale)
90
+ : fetchedProduct
91
+ if (!product) notFound()
92
+
93
+ const [variants, settings] = await Promise.all([
94
+ findProductVariants(product.id).catch(() => []),
95
+ findSettings().catch(() => null),
96
+ ])
97
+
98
+ // Mirror the canonical out-of-stock logic (see buildOutOfStockCondition in
99
+ // the SDK): variant products are in stock if any variant has inventory;
100
+ // non-variant products honor trackInventory + continue-selling.
101
+ const inStock = product.hasVariants
102
+ ? variants.some((v) => (v.inventory ?? 0) > 0)
103
+ : !product.trackInventory ||
104
+ (product.inventory ?? 0) > 0 ||
105
+ !!product.continueSellingWhenOutOfStock
106
+
107
+ const jsonLd = buildProductJsonLd(
108
+ {
109
+ name: product.name,
110
+ slug: product.slug,
111
+ description: product.description,
112
+ price: product.price,
113
+ images: product.images,
114
+ sku: product.sku,
115
+ inStock,
116
+ },
117
+ {
118
+ storeUrl: STORE_URL,
119
+ storeName: settings?.storeName || process.env.STORE_NAME || 'Store',
120
+ currency: (settings?.storeCurrency || 'USD').toUpperCase(),
121
+ }
122
+ )
123
+
124
+ // Breadcrumb trail (Home › Products › product) — server-rendered. Only when
125
+ // we have an absolute origin to build valid `item` URLs.
126
+ const breadcrumbJsonLd = STORE_URL
127
+ ? buildBreadcrumbJsonLd([
128
+ { name: 'Home', url: `${STORE_URL}/` },
129
+ { name: 'Products', url: `${STORE_URL}/products` },
130
+ { name: product.name, url: `${STORE_URL}/products/${product.slug}` },
131
+ ])
132
+ : null
133
+
134
+ return (
135
+ <>
136
+ <script
137
+ type="application/ld+json"
138
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
139
+ />
140
+ {breadcrumbJsonLd && (
141
+ <script
142
+ type="application/ld+json"
143
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
144
+ />
145
+ )}
146
+ <ProductDetailClient slug={slug} />
147
+ </>
148
+ )
149
+ }
@@ -0,0 +1,26 @@
1
+ import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
2
+ import { getT } from '@rovela-ai/sdk/core'
3
+ import type { Metadata } from 'next'
4
+
5
+ /**
6
+ * Server layout for the /products listing. The listing page is a client
7
+ * component and can't export metadata, so this wrapper supplies the canonical
8
+ * for /products. The product detail page (/products/[slug]) sets its OWN
9
+ * per-product canonical in generateMetadata, which overrides this for those
10
+ * routes — so it never mis-canonicalizes a product page to the listing.
11
+ */
12
+ export async function generateMetadata() {
13
+ const { locale } = await getRequestLocale()
14
+ return {
15
+ title: getT(locale)('listing.title'),
16
+ alternates: await localizedAlternates('/products'),
17
+ }
18
+ }
19
+
20
+ export default function ProductsLayout({
21
+ children,
22
+ }: {
23
+ children: React.ReactNode
24
+ }) {
25
+ return children
26
+ }