@rovela-ai/sdk 0.19.4 → 0.19.5

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 (202) hide show
  1. package/docs/changelog/README.md +53 -0
  2. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  3. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  4. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  5. package/docs/changelog/epoch-04-emails.md +65 -0
  6. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  7. package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
  8. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  9. package/docs/design/admin-sidebar-and-band.html +542 -0
  10. package/package.json +3 -1
  11. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  12. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  13. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  14. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  15. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  16. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  17. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  18. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  19. package/templates/store-template/.env.example +50 -0
  20. package/templates/store-template/.mcp.json +8 -0
  21. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  22. package/templates/store-template/CLAUDE.md +14 -0
  23. package/templates/store-template/app/LayoutContent.tsx +106 -0
  24. package/templates/store-template/app/[...path]/page.tsx +79 -0
  25. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  26. package/templates/store-template/app/account/layout.tsx +17 -0
  27. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  28. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  29. package/templates/store-template/app/account/orders/page.tsx +153 -0
  30. package/templates/store-template/app/account/page.tsx +184 -0
  31. package/templates/store-template/app/account/profile/page.tsx +434 -0
  32. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  33. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  34. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  35. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  36. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  37. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  38. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  39. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  40. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  41. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  42. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  43. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  44. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  45. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  46. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  47. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  48. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  49. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  50. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  51. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  52. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  53. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  54. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  55. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  56. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  57. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  58. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  59. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  60. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  61. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  62. package/templates/store-template/app/admin/layout.tsx +47 -0
  63. package/templates/store-template/app/admin/login/page.tsx +528 -0
  64. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  65. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  66. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  67. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  68. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  69. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  70. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  71. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  72. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  73. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  74. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  75. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  76. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  77. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  78. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  79. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  80. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  81. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  82. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  83. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  84. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  85. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  86. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  87. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  88. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  89. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  90. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  91. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  92. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  93. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  94. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  95. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  96. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  97. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  98. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  99. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  100. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  101. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  102. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  103. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  104. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  105. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  106. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  107. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  108. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  109. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  110. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  111. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  112. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  113. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  115. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  116. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  117. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  118. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  119. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  120. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  121. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  122. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  124. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  125. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  126. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  127. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  128. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  129. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  130. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  131. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  132. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  133. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  134. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  135. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  136. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  137. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  138. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  139. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  140. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  141. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  142. package/templates/store-template/app/api/categories/route.ts +10 -0
  143. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  144. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  145. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  146. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  147. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  148. package/templates/store-template/app/api/checkout/route.ts +10 -0
  149. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  150. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  151. package/templates/store-template/app/api/contact/route.ts +164 -0
  152. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  153. package/templates/store-template/app/api/media/route.ts +7 -0
  154. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  155. package/templates/store-template/app/api/products/route.ts +10 -0
  156. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  157. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  158. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  159. package/templates/store-template/app/auth/layout.tsx +17 -0
  160. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  161. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  162. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  163. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  164. package/templates/store-template/app/cart/page.tsx +137 -0
  165. package/templates/store-template/app/checkout/page.tsx +88 -0
  166. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  167. package/templates/store-template/app/contact/layout.tsx +24 -0
  168. package/templates/store-template/app/contact/page.tsx +321 -0
  169. package/templates/store-template/app/faq/page.tsx +153 -0
  170. package/templates/store-template/app/globals.css +99 -0
  171. package/templates/store-template/app/layout.tsx +269 -0
  172. package/templates/store-template/app/not-found.tsx +35 -0
  173. package/templates/store-template/app/page.tsx +23 -0
  174. package/templates/store-template/app/privacy/page.tsx +281 -0
  175. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  176. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  177. package/templates/store-template/app/products/layout.tsx +26 -0
  178. package/templates/store-template/app/products/page.tsx +102 -0
  179. package/templates/store-template/app/robots.ts +25 -0
  180. package/templates/store-template/app/sitemap.ts +76 -0
  181. package/templates/store-template/app/terms/page.tsx +313 -0
  182. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  183. package/templates/store-template/components/Providers.tsx +37 -0
  184. package/templates/store-template/components/layout/Footer.tsx +150 -0
  185. package/templates/store-template/components/layout/Header.tsx +162 -0
  186. package/templates/store-template/components/sections/.gitkeep +7 -0
  187. package/templates/store-template/components/sections/CTA.tsx +51 -0
  188. package/templates/store-template/components/sections/Features.tsx +91 -0
  189. package/templates/store-template/components/sections/Hero.tsx +85 -0
  190. package/templates/store-template/eslint.config.mjs +18 -0
  191. package/templates/store-template/lib/store-config.ts +77 -0
  192. package/templates/store-template/lib/store-policy.ts +68 -0
  193. package/templates/store-template/lib/utils.ts +21 -0
  194. package/templates/store-template/next-env.d.ts +6 -0
  195. package/templates/store-template/next.config.js +105 -0
  196. package/templates/store-template/package.json +32 -0
  197. package/templates/store-template/postcss.config.js +6 -0
  198. package/templates/store-template/proxy.ts +150 -0
  199. package/templates/store-template/public/rovela-logo.png +0 -0
  200. package/templates/store-template/tailwind.config.ts +74 -0
  201. package/templates/store-template/tsconfig.json +41 -0
  202. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,434 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Customer Profile Page
5
+ *
6
+ * Admin identity cannot reach this page: `useAuth()` role-filters the
7
+ * session (returns null for admin JWTs) and `AuthGuard` redirects anything
8
+ * that's not a signed-in customer to `/auth/signin`.
9
+ */
10
+
11
+ import { useState, useEffect } from 'react'
12
+ import { LocaleLink as Link } from '@rovela-ai/sdk/core'
13
+ import { ChevronLeft, User, Mail, Phone, Lock, Check, AlertCircle } from 'lucide-react'
14
+ import { useSession } from 'next-auth/react'
15
+ import { AuthGuard, useAuth } from '@rovela-ai/sdk/auth'
16
+ import { useStoreSettings } from '@rovela-ai/sdk/core'
17
+ import { useT } from '@rovela-ai/sdk/core'
18
+
19
+ interface Profile {
20
+ id: string
21
+ email: string
22
+ name: string | null
23
+ phone: string | null
24
+ emailVerified: boolean
25
+ createdAt: string
26
+ }
27
+
28
+ function ProfileContent() {
29
+ const t = useT()
30
+ const { update: updateSession } = useSession()
31
+ const { customer } = useAuth()
32
+ const { formatDate } = useStoreSettings()
33
+
34
+ // Profile state
35
+ const [profile, setProfile] = useState<Profile | null>(null)
36
+ const [isLoading, setIsLoading] = useState(true)
37
+
38
+ // Edit profile form state
39
+ const [isEditingProfile, setIsEditingProfile] = useState(false)
40
+ const [editName, setEditName] = useState('')
41
+ const [editEmail, setEditEmail] = useState('')
42
+ const [editPhone, setEditPhone] = useState('')
43
+ const [profileSaving, setProfileSaving] = useState(false)
44
+ const [profileError, setProfileError] = useState('')
45
+ const [profileSuccess, setProfileSuccess] = useState('')
46
+
47
+ // Change password form state
48
+ const [currentPassword, setCurrentPassword] = useState('')
49
+ const [newPassword, setNewPassword] = useState('')
50
+ const [confirmPassword, setConfirmPassword] = useState('')
51
+ const [passwordSaving, setPasswordSaving] = useState(false)
52
+ const [passwordError, setPasswordError] = useState('')
53
+ const [passwordSuccess, setPasswordSuccess] = useState('')
54
+
55
+ // Fetch profile on mount
56
+ useEffect(() => {
57
+ async function fetchProfile() {
58
+ try {
59
+ const res = await fetch('/api/account/profile')
60
+ if (res.ok) {
61
+ const data = await res.json()
62
+ setProfile(data.profile)
63
+ setEditName(data.profile.name || '')
64
+ setEditEmail(data.profile.email)
65
+ setEditPhone(data.profile.phone || '')
66
+ }
67
+ } catch (error) {
68
+ console.error('Failed to fetch profile:', error)
69
+ } finally {
70
+ setIsLoading(false)
71
+ }
72
+ }
73
+ fetchProfile()
74
+ }, [])
75
+
76
+ // Handle profile update
77
+ const handleProfileSubmit = async (e: React.FormEvent) => {
78
+ e.preventDefault()
79
+ setProfileError('')
80
+ setProfileSuccess('')
81
+ setProfileSaving(true)
82
+
83
+ try {
84
+ const res = await fetch('/api/account/profile', {
85
+ method: 'PUT',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({
88
+ name: editName.trim() || null,
89
+ email: editEmail.trim(),
90
+ phone: editPhone.trim() || null,
91
+ }),
92
+ })
93
+
94
+ const data = await res.json()
95
+
96
+ if (!res.ok) {
97
+ setProfileError(data.error || t('account.profileUpdateFailed'))
98
+ return
99
+ }
100
+
101
+ setProfile(data.profile)
102
+ setProfileSuccess(t('account.profileUpdated'))
103
+ setIsEditingProfile(false)
104
+
105
+ // Update session if name changed
106
+ if (data.profile.name !== customer?.name) {
107
+ await updateSession({ name: data.profile.name })
108
+ }
109
+
110
+ // Clear success message after 3 seconds
111
+ setTimeout(() => setProfileSuccess(''), 3000)
112
+ } catch (error) {
113
+ setProfileError(t('account.genericError'))
114
+ } finally {
115
+ setProfileSaving(false)
116
+ }
117
+ }
118
+
119
+ // Handle password change
120
+ const handlePasswordSubmit = async (e: React.FormEvent) => {
121
+ e.preventDefault()
122
+ setPasswordError('')
123
+ setPasswordSuccess('')
124
+
125
+ // Validate passwords match
126
+ if (newPassword !== confirmPassword) {
127
+ setPasswordError(t('auth.passwordsNoMatch'))
128
+ return
129
+ }
130
+
131
+ // Validate password length
132
+ if (newPassword.length < 8) {
133
+ setPasswordError(t('auth.password8'))
134
+ return
135
+ }
136
+
137
+ setPasswordSaving(true)
138
+
139
+ try {
140
+ const res = await fetch('/api/account/change-password', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({
144
+ currentPassword,
145
+ newPassword,
146
+ }),
147
+ })
148
+
149
+ const data = await res.json()
150
+
151
+ if (!res.ok) {
152
+ setPasswordError(data.error || t('account.passwordChangeFailed'))
153
+ return
154
+ }
155
+
156
+ setPasswordSuccess(t('account.passwordChanged'))
157
+ setCurrentPassword('')
158
+ setNewPassword('')
159
+ setConfirmPassword('')
160
+
161
+ // Clear success message after 3 seconds
162
+ setTimeout(() => setPasswordSuccess(''), 3000)
163
+ } catch (error) {
164
+ setPasswordError(t('account.genericError'))
165
+ } finally {
166
+ setPasswordSaving(false)
167
+ }
168
+ }
169
+
170
+ // Cancel editing
171
+ const handleCancelEdit = () => {
172
+ setIsEditingProfile(false)
173
+ setEditName(profile?.name || '')
174
+ setEditEmail(profile?.email || '')
175
+ setEditPhone(profile?.phone || '')
176
+ setProfileError('')
177
+ }
178
+
179
+ if (isLoading) {
180
+ return (
181
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
182
+ <div className="animate-pulse space-y-6">
183
+ <div className="h-8 w-32 bg-muted rounded" />
184
+ <div className="h-64 bg-muted rounded" />
185
+ <div className="h-64 bg-muted rounded" />
186
+ </div>
187
+ </div>
188
+ )
189
+ }
190
+
191
+ return (
192
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-2xl">
193
+ {/* Back Navigation */}
194
+ <nav className="mb-8">
195
+ <Link
196
+ href="/account"
197
+ className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
198
+ >
199
+ <ChevronLeft className="h-4 w-4 mr-1" />
200
+ {t('account.backToAccount')}
201
+ </Link>
202
+ </nav>
203
+
204
+ <h1 className="text-3xl font-bold font-heading mb-8">{t('account.myProfile')}</h1>
205
+
206
+ {/* Profile Info Section */}
207
+ <section className="rounded-lg border bg-card p-6 mb-6">
208
+ <div className="flex items-center justify-between mb-6">
209
+ <h2 className="text-xl font-semibold flex items-center gap-2">
210
+ <User className="h-5 w-5" />
211
+ {t('account.profileInformation')}
212
+ </h2>
213
+ {!isEditingProfile && (
214
+ <button
215
+ onClick={() => setIsEditingProfile(true)}
216
+ className="text-sm text-primary hover:underline"
217
+ >
218
+ {t('account.edit')}
219
+ </button>
220
+ )}
221
+ </div>
222
+
223
+ {/* Success/Error Messages */}
224
+ {profileSuccess && (
225
+ <div className="mb-4 p-3 rounded-md bg-green-50 text-green-800 flex items-center gap-2">
226
+ <Check className="h-4 w-4" />
227
+ {profileSuccess}
228
+ </div>
229
+ )}
230
+ {profileError && (
231
+ <div className="mb-4 p-3 rounded-md bg-red-50 text-red-800 flex items-center gap-2">
232
+ <AlertCircle className="h-4 w-4" />
233
+ {profileError}
234
+ </div>
235
+ )}
236
+
237
+ {isEditingProfile ? (
238
+ <form onSubmit={handleProfileSubmit} className="space-y-4">
239
+ <div>
240
+ <label htmlFor="name" className="block text-sm font-medium mb-1">
241
+ {t('account.name')}
242
+ </label>
243
+ <input
244
+ type="text"
245
+ id="name"
246
+ value={editName}
247
+ onChange={(e) => setEditName(e.target.value)}
248
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
249
+ placeholder={t('account.namePlaceholder')}
250
+ maxLength={100}
251
+ />
252
+ </div>
253
+
254
+ <div>
255
+ <label htmlFor="email" className="block text-sm font-medium mb-1">
256
+ {t('account.email')}
257
+ </label>
258
+ <input
259
+ type="email"
260
+ id="email"
261
+ value={editEmail}
262
+ onChange={(e) => setEditEmail(e.target.value)}
263
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
264
+ placeholder={t('checkout.emailPlaceholder')}
265
+ required
266
+ />
267
+ </div>
268
+
269
+ <div>
270
+ <label htmlFor="phone" className="block text-sm font-medium mb-1">
271
+ {t('account.phone')}
272
+ </label>
273
+ <input
274
+ type="tel"
275
+ id="phone"
276
+ value={editPhone}
277
+ onChange={(e) => setEditPhone(e.target.value)}
278
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
279
+ placeholder={t('account.phonePlaceholder')}
280
+ maxLength={40}
281
+ />
282
+ </div>
283
+
284
+ <div className="flex gap-3">
285
+ <button
286
+ type="submit"
287
+ disabled={profileSaving}
288
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
289
+ >
290
+ {profileSaving ? t('account.saving') : t('account.saveChanges')}
291
+ </button>
292
+ <button
293
+ type="button"
294
+ onClick={handleCancelEdit}
295
+ className="rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors"
296
+ >
297
+ {t('account.cancel')}
298
+ </button>
299
+ </div>
300
+ </form>
301
+ ) : (
302
+ <div className="space-y-4">
303
+ <div className="flex items-center gap-3">
304
+ <User className="h-4 w-4 text-muted-foreground" />
305
+ <div>
306
+ <p className="text-sm text-muted-foreground">{t('account.name')}</p>
307
+ <p className="font-medium">{profile?.name || t('account.notSet')}</p>
308
+ </div>
309
+ </div>
310
+
311
+ <div className="flex items-center gap-3">
312
+ <Mail className="h-4 w-4 text-muted-foreground" />
313
+ <div>
314
+ <p className="text-sm text-muted-foreground">{t('account.email')}</p>
315
+ <p className="font-medium">{profile?.email}</p>
316
+ {profile?.emailVerified ? (
317
+ <span className="inline-flex items-center text-xs text-green-600 mt-1">
318
+ <Check className="h-3 w-3 mr-1" />
319
+ {t('account.verified')}
320
+ </span>
321
+ ) : (
322
+ <span className="text-xs text-amber-600 mt-1">
323
+ {t('account.notVerified')}
324
+ </span>
325
+ )}
326
+ </div>
327
+ </div>
328
+
329
+ <div className="flex items-center gap-3">
330
+ <Phone className="h-4 w-4 text-muted-foreground" />
331
+ <div>
332
+ <p className="text-sm text-muted-foreground">{t('account.phone')}</p>
333
+ <p className="font-medium">{profile?.phone || t('account.notSet')}</p>
334
+ </div>
335
+ </div>
336
+
337
+ <div className="pt-4 border-t">
338
+ <p className="text-sm text-muted-foreground">
339
+ {t('account.memberSince', { date: profile?.createdAt ? formatDate(new Date(profile.createdAt)) : t('account.unknown') })}
340
+ </p>
341
+ </div>
342
+ </div>
343
+ )}
344
+ </section>
345
+
346
+ {/* Change Password Section */}
347
+ <section className="rounded-lg border bg-card p-6">
348
+ <h2 className="text-xl font-semibold flex items-center gap-2 mb-6">
349
+ <Lock className="h-5 w-5" />
350
+ {t('account.changePassword')}
351
+ </h2>
352
+
353
+ {/* Success/Error Messages */}
354
+ {passwordSuccess && (
355
+ <div className="mb-4 p-3 rounded-md bg-green-50 text-green-800 flex items-center gap-2">
356
+ <Check className="h-4 w-4" />
357
+ {passwordSuccess}
358
+ </div>
359
+ )}
360
+ {passwordError && (
361
+ <div className="mb-4 p-3 rounded-md bg-red-50 text-red-800 flex items-center gap-2">
362
+ <AlertCircle className="h-4 w-4" />
363
+ {passwordError}
364
+ </div>
365
+ )}
366
+
367
+ <form onSubmit={handlePasswordSubmit} className="space-y-4">
368
+ <div>
369
+ <label htmlFor="currentPassword" className="block text-sm font-medium mb-1">
370
+ {t('account.currentPassword')}
371
+ </label>
372
+ <input
373
+ type="password"
374
+ id="currentPassword"
375
+ value={currentPassword}
376
+ onChange={(e) => setCurrentPassword(e.target.value)}
377
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
378
+ required
379
+ />
380
+ </div>
381
+
382
+ <div>
383
+ <label htmlFor="newPassword" className="block text-sm font-medium mb-1">
384
+ {t('account.newPassword')}
385
+ </label>
386
+ <input
387
+ type="password"
388
+ id="newPassword"
389
+ value={newPassword}
390
+ onChange={(e) => setNewPassword(e.target.value)}
391
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
392
+ minLength={8}
393
+ required
394
+ />
395
+ <p className="text-xs text-muted-foreground mt-1">
396
+ {t('account.password8Hint')}
397
+ </p>
398
+ </div>
399
+
400
+ <div>
401
+ <label htmlFor="confirmPassword" className="block text-sm font-medium mb-1">
402
+ {t('account.confirmNewPassword')}
403
+ </label>
404
+ <input
405
+ type="password"
406
+ id="confirmPassword"
407
+ value={confirmPassword}
408
+ onChange={(e) => setConfirmPassword(e.target.value)}
409
+ className="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
410
+ minLength={8}
411
+ required
412
+ />
413
+ </div>
414
+
415
+ <button
416
+ type="submit"
417
+ disabled={passwordSaving || !currentPassword || !newPassword || !confirmPassword}
418
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
419
+ >
420
+ {passwordSaving ? t('account.changing') : t('account.changePassword')}
421
+ </button>
422
+ </form>
423
+ </section>
424
+ </div>
425
+ )
426
+ }
427
+
428
+ export default function ProfilePage() {
429
+ return (
430
+ <AuthGuard redirectTo="/auth/signin">
431
+ <ProfileContent />
432
+ </AuthGuard>
433
+ )
434
+ }
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Admin Account Page
5
+ *
6
+ * Self-service profile + password management for logged-in admins.
7
+ * Mounts the SDK's AdminAccountPage component inside the dashboard
8
+ * layout. All validation, invariant enforcement, and error handling
9
+ * happen at the service + API layer — this page is purely a wrapper.
10
+ */
11
+
12
+ import { AdminAccountPage } from '@rovela-ai/sdk/admin'
13
+
14
+ export default function AdminAccountRoute() {
15
+ return <AdminAccountPage />
16
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Insights → Events tab. Raw event log browser.
5
+ *
6
+ * See CLAUDE.md §46.
7
+ */
8
+
9
+ import { EventsView, useAnalyticsPeriod } from '@rovela-ai/sdk/analytics'
10
+
11
+ export default function AdminAnalyticsEventsPage() {
12
+ const { period } = useAnalyticsPeriod()
13
+ return <EventsView period={period} />
14
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Analytics layout — owns the page header, tab nav and period state shared
5
+ * across the three tabs (Dashboards, Events, Visitors). Period selection
6
+ * survives tab nav because Next.js App Router preserves layout instances
7
+ * across child route changes.
8
+ *
9
+ * The AdminPageHeader owns the title (nav and page now both say
10
+ * "Analytics"); AnalyticsTabNav renders with title={null} so only the tab
11
+ * strip appears — never a second title (LEDGER page architecture).
12
+ *
13
+ * See CLAUDE.md §46.
14
+ */
15
+
16
+ import {
17
+ AnalyticsPeriodProvider,
18
+ AnalyticsTabNav,
19
+ useAnalyticsPeriod,
20
+ } from '@rovela-ai/sdk/analytics'
21
+ import { AdminPageHeader, PeriodSelector } from '@rovela-ai/sdk/admin'
22
+
23
+ export default function AnalyticsLayout({
24
+ children,
25
+ }: {
26
+ children: React.ReactNode
27
+ }) {
28
+ return (
29
+ <AnalyticsPeriodProvider>
30
+ <div className="space-y-6">
31
+ <AnalyticsHeader />
32
+ <AnalyticsTabNav baseHref="/admin/analytics" title={null} />
33
+ {children}
34
+ </div>
35
+ </AnalyticsPeriodProvider>
36
+ )
37
+ }
38
+
39
+ function AnalyticsHeader() {
40
+ const { period, setPeriod } = useAnalyticsPeriod()
41
+ return (
42
+ <AdminPageHeader
43
+ title="Analytics"
44
+ subtitle="Visitors, sessions, and conversion for your storefront."
45
+ actions={<PeriodSelector value={period} onChange={setPeriod} />}
46
+ />
47
+ )
48
+ }
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Insights → Dashboards tab. Period flows in via the shared layout's
5
+ * AnalyticsPeriodProvider so the three tabs stay in sync.
6
+ *
7
+ * See CLAUDE.md §46.
8
+ */
9
+
10
+ import { DashboardsView, useAnalyticsPeriod } from '@rovela-ai/sdk/analytics'
11
+
12
+ export default function AdminAnalyticsDashboardsPage() {
13
+ const { period } = useAnalyticsPeriod()
14
+ return <DashboardsView period={period} />
15
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Insights → Visitors tab. Per-visitor aggregate browser.
5
+ *
6
+ * See CLAUDE.md §46.
7
+ */
8
+
9
+ import { VisitorsView, useAnalyticsPeriod } from '@rovela-ai/sdk/analytics'
10
+
11
+ export default function AdminAnalyticsVisitorsPage() {
12
+ const { period } = useAnalyticsPeriod()
13
+ return <VisitorsView period={period} />
14
+ }
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Admin Edit Category Page
5
+ *
6
+ * Edit an existing category using the SDK CategoryForm component.
7
+ * Uses useParams() hook for client-side dynamic route access.
8
+ */
9
+
10
+ import { useParams, useRouter } from 'next/navigation'
11
+ import { AdminPageHeader, CategoryForm } from '@rovela-ai/sdk/admin'
12
+
13
+ export default function EditCategoryPage() {
14
+ const params = useParams()
15
+ const id = params.id as string
16
+ const router = useRouter()
17
+
18
+ return (
19
+ <div className="space-y-6">
20
+ <AdminPageHeader
21
+ backHref="/admin/categories"
22
+ backLabel="Back to categories"
23
+ eyebrow="Categories"
24
+ title="Edit category"
25
+ />
26
+
27
+ <CategoryForm
28
+ categoryId={id}
29
+ onSuccess={() => {
30
+ router.push('/admin/categories')
31
+ }}
32
+ onCancel={() => {
33
+ router.push('/admin/categories')
34
+ }}
35
+ />
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Admin New Category Page
5
+ *
6
+ * Create a new category using the SDK CategoryForm component.
7
+ */
8
+
9
+ import { useRouter } from 'next/navigation'
10
+ import { AdminPageHeader, CategoryForm } from '@rovela-ai/sdk/admin'
11
+
12
+ export default function NewCategoryPage() {
13
+ const router = useRouter()
14
+
15
+ return (
16
+ <div className="space-y-6">
17
+ <AdminPageHeader
18
+ backHref="/admin/categories"
19
+ backLabel="Back to categories"
20
+ eyebrow="Categories"
21
+ title="Add category"
22
+ subtitle="Create a collection your customers can browse."
23
+ />
24
+
25
+ <CategoryForm
26
+ onSuccess={() => {
27
+ router.push('/admin/categories')
28
+ }}
29
+ onCancel={() => {
30
+ router.push('/admin/categories')
31
+ }}
32
+ />
33
+ </div>
34
+ )
35
+ }