@rovela-ai/sdk 0.19.3 → 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.
- package/dist/admin/server/admin-session.d.ts.map +1 -1
- package/dist/admin/server/admin-session.js +7 -0
- package/dist/admin/server/admin-session.js.map +1 -1
- package/dist/auth/server/customer-session.d.ts.map +1 -1
- package/dist/auth/server/customer-session.js +5 -0
- package/dist/auth/server/customer-session.js.map +1 -1
- package/dist/checkout/server/checkout-customer.d.ts.map +1 -1
- package/dist/checkout/server/checkout-customer.js +5 -0
- package/dist/checkout/server/checkout-customer.js.map +1 -1
- package/docs/changelog/README.md +53 -0
- package/docs/changelog/epoch-01-chrome-consent.md +63 -0
- package/docs/changelog/epoch-02-platform-commerce.md +108 -0
- package/docs/changelog/epoch-03-i18n-files.md +72 -0
- package/docs/changelog/epoch-04-emails.md +65 -0
- package/docs/changelog/epoch-05-admin-experience.md +98 -0
- package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
- package/docs/design/admin-ledger-visual-universe.html +1961 -0
- package/docs/design/admin-sidebar-and-band.html +542 -0
- package/package.json +3 -1
- package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
- package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
- package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
- package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
- package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
- package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
- package/templates/store-template/.env.example +50 -0
- package/templates/store-template/.mcp.json +8 -0
- package/templates/store-template/.rovela/blueprint.example.json +190 -0
- package/templates/store-template/CLAUDE.md +14 -0
- package/templates/store-template/app/LayoutContent.tsx +106 -0
- package/templates/store-template/app/[...path]/page.tsx +79 -0
- package/templates/store-template/app/account/addresses/page.tsx +45 -0
- package/templates/store-template/app/account/layout.tsx +17 -0
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
- package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
- package/templates/store-template/app/account/orders/page.tsx +153 -0
- package/templates/store-template/app/account/page.tsx +184 -0
- package/templates/store-template/app/account/profile/page.tsx +434 -0
- package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
- package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
- package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
- package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
- package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
- package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
- package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
- package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
- package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
- package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
- package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
- package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
- package/templates/store-template/app/admin/layout.tsx +47 -0
- package/templates/store-template/app/admin/login/page.tsx +528 -0
- package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
- package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
- package/templates/store-template/app/api/account/addresses/route.ts +10 -0
- package/templates/store-template/app/api/account/change-password/route.ts +107 -0
- package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
- package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
- package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
- package/templates/store-template/app/api/account/orders/route.ts +29 -0
- package/templates/store-template/app/api/account/profile/route.ts +165 -0
- package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
- package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
- package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
- package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/route.ts +9 -0
- package/templates/store-template/app/api/admin/check/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/customers/route.ts +9 -0
- package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
- package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
- package/templates/store-template/app/api/admin/emails/route.ts +5 -0
- package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
- package/templates/store-template/app/api/admin/files/route.ts +8 -0
- package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
- package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
- package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/locations/route.ts +10 -0
- package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
- package/templates/store-template/app/api/admin/me/route.ts +11 -0
- package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/orders/route.ts +9 -0
- package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
- package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
- package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/admin/settings/route.ts +11 -0
- package/templates/store-template/app/api/admin/setup/route.ts +10 -0
- package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
- package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
- package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
- package/templates/store-template/app/api/admin/stats/route.ts +9 -0
- package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
- package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
- package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
- package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
- package/templates/store-template/app/api/admin/users/route.ts +12 -0
- package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
- package/templates/store-template/app/api/analytics/track/route.ts +12 -0
- package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
- package/templates/store-template/app/api/auth/register/route.ts +9 -0
- package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
- package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
- package/templates/store-template/app/api/categories/route.ts +10 -0
- package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
- package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
- package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
- package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
- package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
- package/templates/store-template/app/api/checkout/route.ts +10 -0
- package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
- package/templates/store-template/app/api/checkout/status/route.ts +10 -0
- package/templates/store-template/app/api/contact/route.ts +164 -0
- package/templates/store-template/app/api/media/presign/route.ts +7 -0
- package/templates/store-template/app/api/media/route.ts +7 -0
- package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
- package/templates/store-template/app/api/products/route.ts +10 -0
- package/templates/store-template/app/api/store/settings/route.ts +12 -0
- package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
- package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
- package/templates/store-template/app/auth/layout.tsx +17 -0
- package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
- package/templates/store-template/app/auth/signin/page.tsx +62 -0
- package/templates/store-template/app/auth/signup/page.tsx +38 -0
- package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
- package/templates/store-template/app/cart/page.tsx +137 -0
- package/templates/store-template/app/checkout/page.tsx +88 -0
- package/templates/store-template/app/checkout/success/page.tsx +50 -0
- package/templates/store-template/app/contact/layout.tsx +24 -0
- package/templates/store-template/app/contact/page.tsx +321 -0
- package/templates/store-template/app/faq/page.tsx +153 -0
- package/templates/store-template/app/globals.css +99 -0
- package/templates/store-template/app/layout.tsx +269 -0
- package/templates/store-template/app/not-found.tsx +35 -0
- package/templates/store-template/app/page.tsx +23 -0
- package/templates/store-template/app/privacy/page.tsx +281 -0
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
- package/templates/store-template/app/products/[slug]/page.tsx +149 -0
- package/templates/store-template/app/products/layout.tsx +26 -0
- package/templates/store-template/app/products/page.tsx +102 -0
- package/templates/store-template/app/robots.ts +25 -0
- package/templates/store-template/app/sitemap.ts +76 -0
- package/templates/store-template/app/terms/page.tsx +313 -0
- package/templates/store-template/components/PathSyncBridge.tsx +61 -0
- package/templates/store-template/components/Providers.tsx +37 -0
- package/templates/store-template/components/layout/Footer.tsx +150 -0
- package/templates/store-template/components/layout/Header.tsx +162 -0
- package/templates/store-template/components/sections/.gitkeep +7 -0
- package/templates/store-template/components/sections/CTA.tsx +51 -0
- package/templates/store-template/components/sections/Features.tsx +91 -0
- package/templates/store-template/components/sections/Hero.tsx +85 -0
- package/templates/store-template/eslint.config.mjs +18 -0
- package/templates/store-template/lib/store-config.ts +77 -0
- package/templates/store-template/lib/store-policy.ts +68 -0
- package/templates/store-template/lib/utils.ts +21 -0
- package/templates/store-template/next-env.d.ts +6 -0
- package/templates/store-template/next.config.js +105 -0
- package/templates/store-template/package.json +32 -0
- package/templates/store-template/postcss.config.js +6 -0
- package/templates/store-template/proxy.ts +150 -0
- package/templates/store-template/public/rovela-logo.png +0 -0
- package/templates/store-template/tailwind.config.ts +74 -0
- package/templates/store-template/tsconfig.json +41 -0
- 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
|
+
}
|