@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,88 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Checkout Page
5
+ *
6
+ * Checkout behavior depends on store settings:
7
+ * - Shipping enabled: Full pre-checkout flow (Address → Shipping → Review → Stripe)
8
+ * - Shipping disabled: Simple checkout with direct Stripe redirect (no shipping step)
9
+ *
10
+ * When shipping is enabled, uses CheckoutFlow component which:
11
+ * 1. Collects shipping address via ShippingForm
12
+ * 2. Fetches shipping options via POST /api/checkout/shipping
13
+ * 3. Shows shipping options grouped by carrier via ShippingOptions
14
+ * 4. Displays order summary with totals
15
+ * 5. Redirects to Stripe Checkout for payment (card only, address pre-filled)
16
+ *
17
+ * Both manual (zone-based) and Shippo (carrier API) modes are supported.
18
+ */
19
+
20
+ import { useEffect } from 'react'
21
+ import { useRouter } from 'next/navigation'
22
+ import { useAuth } from '@rovela-ai/sdk/auth'
23
+ import { useCart } from '@rovela-ai/sdk/cart'
24
+ import { CheckoutFlow, CheckoutButton } from '@rovela-ai/sdk/checkout'
25
+ import { useStoreSettings, useT, useLocalePath } from '@rovela-ai/sdk/core'
26
+
27
+ export default function CheckoutPage() {
28
+ const t = useT()
29
+ const localePath = useLocalePath()
30
+ const router = useRouter()
31
+ // `useAuth()` filters out admin sessions (returns null if the signed-in
32
+ // user is an admin, not a customer). Using it instead of raw `useSession`
33
+ // prevents an admin's email from being prefilled as the "customer email"
34
+ // at checkout — which would otherwise leak into the Stripe session.
35
+ const { customer } = useAuth()
36
+ const { items } = useCart()
37
+ const { settings, isLoading: isSettingsLoading } = useStoreSettings()
38
+ const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
39
+
40
+ // Check if shipping is enabled
41
+ const shippingEnabled = settings?.shippingEnabled ?? true
42
+
43
+ // Redirect to cart if empty
44
+ useEffect(() => {
45
+ if (itemCount === 0) {
46
+ router.push(localePath('/cart'))
47
+ }
48
+ }, [itemCount, router, localePath])
49
+
50
+ // Don't render checkout if cart is empty
51
+ if (itemCount === 0) {
52
+ return null
53
+ }
54
+
55
+ // When shipping is disabled, show simple checkout (direct to Stripe)
56
+ // This handles the edge case where user navigates directly to /checkout
57
+ if (!isSettingsLoading && !shippingEnabled) {
58
+ return (
59
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-md">
60
+ <h1 className="text-2xl font-bold font-heading mb-4 text-center">{t('checkoutPage.title')}</h1>
61
+ <p className="text-muted-foreground mb-8 text-center">
62
+ {t('checkoutPage.reviewAndPay')}
63
+ </p>
64
+ <CheckoutButton size="lg" className="w-full">
65
+ {t('checkout.proceedToPayment')}
66
+ </CheckoutButton>
67
+ <button
68
+ onClick={() => router.push(localePath('/cart'))}
69
+ className="w-full mt-4 text-sm text-muted-foreground hover:text-foreground transition-colors"
70
+ >
71
+ ← {t('checkoutPage.returnToCart')}
72
+ </button>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ return (
78
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-6xl">
79
+ <h1 className="text-2xl font-bold font-heading mb-8">{t('checkoutPage.title')}</h1>
80
+ <CheckoutFlow
81
+ customerEmail={customer?.email ?? undefined}
82
+ customerId={customer?.id ?? undefined}
83
+ showOrderSummary={true}
84
+ onCancel={() => router.push(localePath('/cart'))}
85
+ />
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Checkout Success Page
5
+ *
6
+ * Displayed after a successful checkout — Stripe redirect OR a manual-payment
7
+ * order (`?orderId=` set by the manual checkout path). Clears the cart and
8
+ * shows the order confirmation.
9
+ */
10
+
11
+ import { Suspense, useEffect, useRef } from 'react'
12
+ import { useSearchParams } from 'next/navigation'
13
+ import { useCart } from '@rovela-ai/sdk/cart'
14
+ import { CheckoutSuccess } from '@rovela-ai/sdk/checkout'
15
+ import { useLocalePath } from '@rovela-ai/sdk/core'
16
+
17
+ function CheckoutSuccessContent() {
18
+ const localePath = useLocalePath()
19
+ const { clearCart } = useCart()
20
+ const hasClearedCart = useRef(false)
21
+ const searchParams = useSearchParams()
22
+ const orderId = searchParams.get('orderId') || undefined
23
+
24
+ // Clear cart on mount (only once)
25
+ useEffect(() => {
26
+ if (!hasClearedCart.current) {
27
+ clearCart()
28
+ hasClearedCart.current = true
29
+ }
30
+ }, [clearCart])
31
+
32
+ return (
33
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
34
+ <CheckoutSuccess
35
+ orderId={orderId}
36
+ continueShoppingHref={localePath('/products')}
37
+ viewOrderHref={localePath(orderId ? `/account/orders/${orderId}` : '/account/orders')}
38
+ />
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export default function CheckoutSuccessPage() {
44
+ // useSearchParams requires a Suspense boundary for static prerender.
45
+ return (
46
+ <Suspense fallback={null}>
47
+ <CheckoutSuccessContent />
48
+ </Suspense>
49
+ )
50
+ }
@@ -0,0 +1,24 @@
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 /contact. The page is a client component and can't export
7
+ * metadata, so this thin wrapper supplies the self-referencing canonical
8
+ * (resolved against the root layout's metadataBase).
9
+ */
10
+ export async function generateMetadata() {
11
+ const { locale } = await getRequestLocale()
12
+ return {
13
+ title: getT(locale)('contactPage.title'),
14
+ alternates: await localizedAlternates('/contact'),
15
+ }
16
+ }
17
+
18
+ export default function ContactLayout({
19
+ children,
20
+ }: {
21
+ children: React.ReactNode
22
+ }) {
23
+ return children
24
+ }
@@ -0,0 +1,321 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Contact Page
5
+ *
6
+ * Contact form that sends messages to the store owner.
7
+ * Uses SDK email system via /api/contact endpoint.
8
+ */
9
+
10
+ import { useState } from 'react'
11
+ import { Mail, Send, CheckCircle, AlertCircle } from 'lucide-react'
12
+ import { useStoreSettings, useT } from '@rovela-ai/sdk/core'
13
+ import { cn, isValidEmail } from '@/lib/utils'
14
+
15
+ interface FormData {
16
+ name: string
17
+ email: string
18
+ subject: string
19
+ message: string
20
+ }
21
+
22
+ interface FormErrors {
23
+ name?: string
24
+ email?: string
25
+ subject?: string
26
+ message?: string
27
+ }
28
+
29
+ type FormStatus = 'idle' | 'submitting' | 'success' | 'error'
30
+
31
+ export default function ContactPage() {
32
+ const t = useT()
33
+ const { storeName } = useStoreSettings()
34
+ const [formData, setFormData] = useState<FormData>({
35
+ name: '',
36
+ email: '',
37
+ subject: '',
38
+ message: '',
39
+ })
40
+ const [errors, setErrors] = useState<FormErrors>({})
41
+ const [status, setStatus] = useState<FormStatus>('idle')
42
+ const [errorMessage, setErrorMessage] = useState('')
43
+
44
+ const validateForm = (): boolean => {
45
+ const newErrors: FormErrors = {}
46
+
47
+ if (!formData.name.trim() || formData.name.trim().length < 2) {
48
+ newErrors.name = t('contactPage.nameMin')
49
+ }
50
+
51
+ if (!formData.email.trim()) {
52
+ newErrors.email = t('form.emailRequired')
53
+ } else if (!isValidEmail(formData.email)) {
54
+ newErrors.email = t('form.emailInvalid')
55
+ }
56
+
57
+ if (!formData.subject.trim() || formData.subject.trim().length < 5) {
58
+ newErrors.subject = t('contactPage.subjectMin')
59
+ }
60
+
61
+ if (!formData.message.trim() || formData.message.trim().length < 10) {
62
+ newErrors.message = t('contactPage.messageMin')
63
+ }
64
+
65
+ setErrors(newErrors)
66
+ return Object.keys(newErrors).length === 0
67
+ }
68
+
69
+ const handleSubmit = async (e: React.FormEvent) => {
70
+ e.preventDefault()
71
+
72
+ if (!validateForm()) {
73
+ return
74
+ }
75
+
76
+ setStatus('submitting')
77
+ setErrorMessage('')
78
+
79
+ try {
80
+ const response = await fetch('/api/contact', {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify(formData),
86
+ })
87
+
88
+ const data = await response.json()
89
+
90
+ if (!response.ok) {
91
+ throw new Error(data.error || t('contactPage.sendFailed'))
92
+ }
93
+
94
+ setStatus('success')
95
+ setFormData({ name: '', email: '', subject: '', message: '' })
96
+ } catch (err) {
97
+ setStatus('error')
98
+ setErrorMessage(err instanceof Error ? err.message : t('contactPage.somethingWrong'))
99
+ }
100
+ }
101
+
102
+ const handleChange = (
103
+ e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
104
+ ) => {
105
+ const { name, value } = e.target
106
+ setFormData((prev) => ({ ...prev, [name]: value }))
107
+ // Clear error when user starts typing
108
+ if (errors[name as keyof FormErrors]) {
109
+ setErrors((prev) => ({ ...prev, [name]: undefined }))
110
+ }
111
+ }
112
+
113
+ return (
114
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
115
+ <div className="max-w-2xl mx-auto">
116
+ {/* Header */}
117
+ <div className="text-center mb-12">
118
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
119
+ <Mail className="h-8 w-8 text-primary" />
120
+ </div>
121
+ <h1 className="text-3xl font-bold font-heading">{t('contactPage.title')}</h1>
122
+ <p className="mt-4 text-muted-foreground">
123
+ {t('contactPage.intro')}
124
+ </p>
125
+ </div>
126
+
127
+ {/* Success State */}
128
+ {status === 'success' && (
129
+ <div className="rounded-lg border border-green-200 bg-green-50 dark:bg-green-950/20 dark:border-green-900 p-6 mb-8">
130
+ <div className="flex items-start gap-4">
131
+ <CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
132
+ <div>
133
+ <h3 className="font-semibold text-green-800 dark:text-green-200">
134
+ {t('contactPage.sentTitle')}
135
+ </h3>
136
+ <p className="mt-1 text-sm text-green-700 dark:text-green-300">
137
+ {t('contactPage.sentBody', { storeName })}
138
+ </p>
139
+ <button
140
+ onClick={() => setStatus('idle')}
141
+ className="mt-4 text-sm font-medium text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 underline"
142
+ >
143
+ {t('contactPage.sendAnother')}
144
+ </button>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ )}
149
+
150
+ {/* Error State */}
151
+ {status === 'error' && (
152
+ <div className="rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/20 dark:border-red-900 p-6 mb-8">
153
+ <div className="flex items-start gap-4">
154
+ <AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
155
+ <div>
156
+ <h3 className="font-semibold text-red-800 dark:text-red-200">
157
+ {t('contactPage.failedTitle')}
158
+ </h3>
159
+ <p className="mt-1 text-sm text-red-700 dark:text-red-300">
160
+ {errorMessage || t('contactPage.failedBody')}
161
+ </p>
162
+ <button
163
+ onClick={() => setStatus('idle')}
164
+ className="mt-4 text-sm font-medium text-red-700 dark:text-red-300 hover:text-red-800 dark:hover:text-red-200 underline"
165
+ >
166
+ {t('contactPage.tryAgain')}
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ {/* Contact Form */}
174
+ {status !== 'success' && (
175
+ <form onSubmit={handleSubmit} className="rounded-lg border bg-card p-6 sm:p-8">
176
+ <div className="space-y-6">
177
+ {/* Name Field */}
178
+ <div>
179
+ <label
180
+ htmlFor="name"
181
+ className="block text-sm font-medium mb-2"
182
+ >
183
+ {t('auth.name')}
184
+ </label>
185
+ <input
186
+ type="text"
187
+ id="name"
188
+ name="name"
189
+ value={formData.name}
190
+ onChange={handleChange}
191
+ className={cn(
192
+ 'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
193
+ errors.name ? 'border-red-500' : 'border-input'
194
+ )}
195
+ placeholder={t('account.namePlaceholder')}
196
+ disabled={status === 'submitting'}
197
+ />
198
+ {errors.name && (
199
+ <p className="mt-1 text-sm text-red-500">{errors.name}</p>
200
+ )}
201
+ </div>
202
+
203
+ {/* Email Field */}
204
+ <div>
205
+ <label
206
+ htmlFor="email"
207
+ className="block text-sm font-medium mb-2"
208
+ >
209
+ {t('form.email')}
210
+ </label>
211
+ <input
212
+ type="email"
213
+ id="email"
214
+ name="email"
215
+ value={formData.email}
216
+ onChange={handleChange}
217
+ className={cn(
218
+ 'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
219
+ errors.email ? 'border-red-500' : 'border-input'
220
+ )}
221
+ placeholder={t('checkout.emailPlaceholder')}
222
+ disabled={status === 'submitting'}
223
+ />
224
+ {errors.email && (
225
+ <p className="mt-1 text-sm text-red-500">{errors.email}</p>
226
+ )}
227
+ </div>
228
+
229
+ {/* Subject Field */}
230
+ <div>
231
+ <label
232
+ htmlFor="subject"
233
+ className="block text-sm font-medium mb-2"
234
+ >
235
+ {t('contactPage.subject')}
236
+ </label>
237
+ <input
238
+ type="text"
239
+ id="subject"
240
+ name="subject"
241
+ value={formData.subject}
242
+ onChange={handleChange}
243
+ className={cn(
244
+ 'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
245
+ errors.subject ? 'border-red-500' : 'border-input'
246
+ )}
247
+ placeholder={t('contactPage.subjectPlaceholder')}
248
+ disabled={status === 'submitting'}
249
+ />
250
+ {errors.subject && (
251
+ <p className="mt-1 text-sm text-red-500">{errors.subject}</p>
252
+ )}
253
+ </div>
254
+
255
+ {/* Message Field */}
256
+ <div>
257
+ <label
258
+ htmlFor="message"
259
+ className="block text-sm font-medium mb-2"
260
+ >
261
+ {t('contactPage.message')}
262
+ </label>
263
+ <textarea
264
+ id="message"
265
+ name="message"
266
+ rows={5}
267
+ value={formData.message}
268
+ onChange={handleChange}
269
+ className={cn(
270
+ 'flex w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors resize-none',
271
+ errors.message ? 'border-red-500' : 'border-input'
272
+ )}
273
+ placeholder={t('contactPage.messagePlaceholder')}
274
+ disabled={status === 'submitting'}
275
+ />
276
+ {errors.message && (
277
+ <p className="mt-1 text-sm text-red-500">{errors.message}</p>
278
+ )}
279
+ </div>
280
+
281
+ {/* Submit Button */}
282
+ <button
283
+ type="submit"
284
+ disabled={status === 'submitting'}
285
+ className={cn(
286
+ 'w-full flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors',
287
+ status === 'submitting' && 'opacity-70 cursor-not-allowed'
288
+ )}
289
+ >
290
+ {status === 'submitting' ? (
291
+ <>
292
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
293
+ {t('contactPage.sending')}
294
+ </>
295
+ ) : (
296
+ <>
297
+ <Send className="h-4 w-4" />
298
+ {t('contactPage.sendMessage')}
299
+ </>
300
+ )}
301
+ </button>
302
+ </div>
303
+ </form>
304
+ )}
305
+
306
+ {/* Additional Contact Info */}
307
+ <div className="mt-12 text-center">
308
+ <p className="text-sm text-muted-foreground">
309
+ {t('contactPage.reachDirectly')}{' '}
310
+ <a
311
+ href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL || 'support@store.com'}`}
312
+ className="text-primary hover:underline"
313
+ >
314
+ {t('contactPage.supportEmail')}
315
+ </a>
316
+ </p>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ )
321
+ }
@@ -0,0 +1,153 @@
1
+ import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
2
+ import type { Metadata } from 'next'
3
+ import { LocaleLink as Link, getT } from '@rovela-ai/sdk/core'
4
+ import { findSettings } from '@rovela-ai/sdk/core/server'
5
+ import { buildFaqJsonLd, serializeJsonLd } from '@rovela-ai/sdk/core'
6
+ import { storeConfig } from '@/lib/store-config'
7
+ import {
8
+ returnWindowDays,
9
+ shipsToCountries,
10
+ freeShippingThreshold,
11
+ supportEmail,
12
+ } from '@/lib/store-policy'
13
+
14
+ /**
15
+ * Store FAQ — server-rendered, with `FAQPage` JSON-LD whose Q&A mirrors the
16
+ * visible content 1:1 (both read from store_settings + lib/store-policy). FAQ
17
+ * markup is the single highest-leverage GEO signal (≈3× AI-citation rate), and
18
+ * AI engines extract answers verbatim, so each answer is self-contained.
19
+ *
20
+ * Server Component (no 'use client') so the schema + content are in the initial
21
+ * HTML — AI crawlers don't execute JavaScript.
22
+ */
23
+
24
+ export async function generateMetadata() {
25
+ const { locale } = await getRequestLocale()
26
+ const t = getT(locale)
27
+ return {
28
+ title: t('faqPage.title'),
29
+ description: t('faqPage.metaDescription'),
30
+ alternates: await localizedAlternates('/faq'),
31
+ }
32
+ }
33
+
34
+ const COUNTRY_NAMES: Record<string, string> = {
35
+ US: 'the United States',
36
+ CA: 'Canada',
37
+ GB: 'the United Kingdom',
38
+ AU: 'Australia',
39
+ NZ: 'New Zealand',
40
+ IE: 'Ireland',
41
+ DE: 'Germany',
42
+ FR: 'France',
43
+ ES: 'Spain',
44
+ IT: 'Italy',
45
+ NL: 'the Netherlands',
46
+ }
47
+
48
+ function formatCountries(codes: string[]): string {
49
+ const names = codes.map((c) => COUNTRY_NAMES[c] || c)
50
+ if (names.length === 1) return names[0]
51
+ if (names.length === 2) return `${names[0]} and ${names[1]}`
52
+ return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`
53
+ }
54
+
55
+ export default async function FaqPage() {
56
+ const { locale } = await getRequestLocale()
57
+ const t = getT(locale)
58
+ const settings = await findSettings().catch(() => null)
59
+
60
+ const countries = shipsToCountries(settings)
61
+ const freeThreshold = freeShippingThreshold(settings)
62
+ const email = supportEmail(settings)
63
+ const returnDays = returnWindowDays()
64
+ const currency = (settings?.storeCurrency || 'USD').toUpperCase()
65
+ const guestCheckout = settings?.guestCheckout !== false
66
+ const processing = String(storeConfig.shipping?.processingTime ?? '1-2-days').replace(/-/g, ' ')
67
+
68
+ const faqs: Array<{ question: string; answer: string }> = [
69
+ {
70
+ question: 'What payment methods do you accept?',
71
+ answer:
72
+ 'We accept major credit and debit cards through our secure checkout. Your payment details are encrypted and handled by our payment provider — we never store full card numbers.',
73
+ },
74
+ {
75
+ question: 'Which countries do you ship to?',
76
+ answer: `We currently ship to ${formatCountries(countries)}.`,
77
+ },
78
+ freeThreshold > 0
79
+ ? {
80
+ question: 'How much does shipping cost?',
81
+ answer: `Shipping is calculated at checkout based on your address. Orders over ${freeThreshold} ${currency} qualify for free shipping.`,
82
+ }
83
+ : {
84
+ question: 'How much does shipping cost?',
85
+ answer:
86
+ 'Shipping is calculated at checkout based on your delivery address and the items in your order.',
87
+ },
88
+ {
89
+ question: 'How long will my order take to arrive?',
90
+ answer: `Orders are typically processed within ${processing} business days and then shipped. Delivery time depends on your location and the carrier.`,
91
+ },
92
+ returnDays > 0
93
+ ? {
94
+ question: 'What is your return policy?',
95
+ answer: `You can return unused items in their original condition within ${returnDays} days of delivery. Return shipping is the customer's responsibility unless the item arrived damaged or incorrect.`,
96
+ }
97
+ : {
98
+ question: 'What is your return policy?',
99
+ answer:
100
+ 'All sales are final. If your item arrives damaged or incorrect, contact us right away and we will make it right.',
101
+ },
102
+ {
103
+ question: 'Do I need an account to place an order?',
104
+ answer: guestCheckout
105
+ ? 'No account is required — you can check out as a guest. Creating an account lets you track orders and check out faster next time.'
106
+ : 'Please create an account at checkout so you can track your order and manage returns.',
107
+ },
108
+ {
109
+ question: 'How can I contact you?',
110
+ answer: email
111
+ ? `Email us at ${email} and we'll get back to you as soon as we can. You can also use our contact page.`
112
+ : `Reach us through our contact page and we'll get back to you as soon as we can.`,
113
+ },
114
+ ]
115
+
116
+ const faqJsonLd = buildFaqJsonLd(faqs)
117
+
118
+ return (
119
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
120
+ <script
121
+ type="application/ld+json"
122
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
123
+ />
124
+ <div className="max-w-3xl mx-auto">
125
+ <div className="mb-12">
126
+ <h1 className="text-3xl font-bold font-heading">{t('faqPage.title')}</h1>
127
+ <p className="mt-4 text-muted-foreground">
128
+ {t('faqPage.subtitle')}
129
+ </p>
130
+ </div>
131
+
132
+ <div className="space-y-10">
133
+ {faqs.map((f) => (
134
+ <section key={f.question}>
135
+ <h2 className="text-xl font-semibold font-heading mb-3">{f.question}</h2>
136
+ <p className="text-muted-foreground leading-relaxed">{f.answer}</p>
137
+ </section>
138
+ ))}
139
+ </div>
140
+
141
+ <div className="mt-12 pt-8 border-t">
142
+ <p className="text-muted-foreground">
143
+ {t('faqPage.stillHaveQuestion')}{' '}
144
+ <Link href="/contact" className="text-primary hover:underline">
145
+ {t('faqPage.contactUsLink')}
146
+ </Link>
147
+ .
148
+ </p>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )
153
+ }