@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.
- package/dist/admin/components/AdminPageHeader.d.ts.map +1 -1
- package/dist/admin/components/AdminPageHeader.js +9 -2
- package/dist/admin/components/AdminPageHeader.js.map +1 -1
- package/dist/admin/components/AdminUserMenu.d.ts.map +1 -1
- package/dist/admin/components/AdminUserMenu.js +2 -1
- package/dist/admin/components/AdminUserMenu.js.map +1 -1
- package/dist/admin/components/CustomerDetails.d.ts.map +1 -1
- package/dist/admin/components/CustomerDetails.js +2 -1
- package/dist/admin/components/CustomerDetails.js.map +1 -1
- package/dist/admin/components/EmailsManager.d.ts.map +1 -1
- package/dist/admin/components/EmailsManager.js +2 -1
- package/dist/admin/components/EmailsManager.js.map +1 -1
- package/dist/admin/components/LowStockAlert.d.ts.map +1 -1
- package/dist/admin/components/LowStockAlert.js +3 -2
- package/dist/admin/components/LowStockAlert.js.map +1 -1
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +2 -1
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ProductTable.d.ts.map +1 -1
- package/dist/admin/components/ProductTable.js +4 -3
- package/dist/admin/components/ProductTable.js.map +1 -1
- package/dist/admin/components/RecentOrders.d.ts.map +1 -1
- package/dist/admin/components/RecentOrders.js +2 -1
- package/dist/admin/components/RecentOrders.js.map +1 -1
- package/dist/admin/components/SEOPreview.d.ts +2 -8
- package/dist/admin/components/SEOPreview.d.ts.map +1 -1
- package/dist/admin/components/SEOPreview.js +32 -5
- package/dist/admin/components/SEOPreview.js.map +1 -1
- package/dist/admin/components/SecondaryMetricsRow.d.ts.map +1 -1
- package/dist/admin/components/SecondaryMetricsRow.js +3 -2
- package/dist/admin/components/SecondaryMetricsRow.js.map +1 -1
- package/dist/admin/components/StatsCards.d.ts.map +1 -1
- package/dist/admin/components/StatsCards.js +2 -1
- package/dist/admin/components/StatsCards.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 +92 -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,223 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Categories Page
|
|
5
|
+
*
|
|
6
|
+
* Category management with tree display.
|
|
7
|
+
* Uses SDK admin useAdminCategories hook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect } from 'react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
import { Plus, Pencil, Trash2, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react'
|
|
13
|
+
import { AdminPageHeader, useAdminCategories, DeleteConfirmDialog, type CategoryTreeItem, adminToast } from '@rovela-ai/sdk/admin'
|
|
14
|
+
import { AdminEmptyState } from '@rovela-ai/sdk/admin/components'
|
|
15
|
+
|
|
16
|
+
export default function AdminCategoriesPage() {
|
|
17
|
+
const { categoryTree, isLoading, error, deleteCategory } = useAdminCategories()
|
|
18
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
|
19
|
+
const [deletingId, setDeletingId] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
// Auto-expand every parent on first load so newly-created subcategories are
|
|
22
|
+
// visible without the merchant hunting for the chevron. Only seeds once
|
|
23
|
+
// (prev.size === 0 guard) so manual collapse state survives subsequent
|
|
24
|
+
// tree updates.
|
|
25
|
+
// Pattern C — populate state from async-loaded data.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (categoryTree.length === 0) return
|
|
28
|
+
const ids = new Set<string>()
|
|
29
|
+
const walk = (nodes: CategoryTreeItem[]) => nodes.forEach(n => {
|
|
30
|
+
if (n.children.length > 0) {
|
|
31
|
+
ids.add(n.id)
|
|
32
|
+
walk(n.children)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
walk(categoryTree)
|
|
36
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
37
|
+
setExpandedIds(prev => (prev.size === 0 ? ids : prev))
|
|
38
|
+
}, [categoryTree])
|
|
39
|
+
|
|
40
|
+
// Delete confirmation state
|
|
41
|
+
const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null)
|
|
42
|
+
const [deleteError, setDeleteError] = useState<string | null>(null)
|
|
43
|
+
|
|
44
|
+
const toggleExpand = (id: string) => {
|
|
45
|
+
setExpandedIds(prev => {
|
|
46
|
+
const next = new Set(prev)
|
|
47
|
+
if (next.has(id)) {
|
|
48
|
+
next.delete(id)
|
|
49
|
+
} else {
|
|
50
|
+
next.add(id)
|
|
51
|
+
}
|
|
52
|
+
return next
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleDeleteClick = (id: string, name: string) => {
|
|
57
|
+
setDeleteTarget({ id, name })
|
|
58
|
+
setDeleteError(null)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleDeleteConfirm = async () => {
|
|
62
|
+
if (!deleteTarget) return
|
|
63
|
+
|
|
64
|
+
setDeletingId(deleteTarget.id)
|
|
65
|
+
const success = await deleteCategory(deleteTarget.id)
|
|
66
|
+
setDeletingId(null)
|
|
67
|
+
|
|
68
|
+
if (success) {
|
|
69
|
+
setDeleteTarget(null)
|
|
70
|
+
adminToast.success('Category deleted')
|
|
71
|
+
} else {
|
|
72
|
+
setDeleteError('Failed to delete category. It may have products assigned to it.')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleDeleteCancel = () => {
|
|
77
|
+
if (!deletingId) {
|
|
78
|
+
setDeleteTarget(null)
|
|
79
|
+
setDeleteError(null)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const renderCategory = (category: CategoryTreeItem, depth: number = 0) => {
|
|
84
|
+
const hasChildren = category.children.length > 0
|
|
85
|
+
const isExpanded = expandedIds.has(category.id)
|
|
86
|
+
const isDeleting = deletingId === category.id
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div key={category.id}>
|
|
90
|
+
<div
|
|
91
|
+
className={`flex items-center gap-3 border-b border-border px-4 py-3 hover:bg-accent/50 transition-colors ${isDeleting ? 'opacity-50' : ''}`}
|
|
92
|
+
style={{ paddingLeft: `${16 + depth * 24}px` }}
|
|
93
|
+
>
|
|
94
|
+
{/* Expand/collapse toggle */}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => toggleExpand(category.id)}
|
|
98
|
+
className={`p-1 rounded hover:bg-accent ${!hasChildren ? 'invisible' : ''}`}
|
|
99
|
+
disabled={!hasChildren}
|
|
100
|
+
>
|
|
101
|
+
{isExpanded ? (
|
|
102
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
103
|
+
) : (
|
|
104
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
105
|
+
)}
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{/* Category icon */}
|
|
109
|
+
<FolderOpen className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
|
110
|
+
|
|
111
|
+
{/* Category info */}
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<span className="font-medium text-foreground truncate">{category.name}</span>
|
|
115
|
+
{hasChildren && (
|
|
116
|
+
<span
|
|
117
|
+
className="text-xs text-muted-foreground tabular-nums"
|
|
118
|
+
aria-label={`${category.children.length} subcategor${category.children.length === 1 ? 'y' : 'ies'}`}
|
|
119
|
+
>
|
|
120
|
+
{category.children.length}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
<div className="text-sm text-muted-foreground truncate">/{category.slug}</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Actions */}
|
|
128
|
+
<div className="flex items-center gap-2">
|
|
129
|
+
<Link
|
|
130
|
+
href={`/admin/categories/${category.id}`}
|
|
131
|
+
className="p-2 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
132
|
+
>
|
|
133
|
+
<Pencil className="h-4 w-4" />
|
|
134
|
+
</Link>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => handleDeleteClick(category.id, category.name)}
|
|
138
|
+
disabled={isDeleting}
|
|
139
|
+
className="p-2 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50"
|
|
140
|
+
>
|
|
141
|
+
<Trash2 className="h-4 w-4" />
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Children */}
|
|
147
|
+
{hasChildren && isExpanded && (
|
|
148
|
+
<div>
|
|
149
|
+
{category.children.map(child => renderCategory(child, depth + 1))}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="space-y-6">
|
|
158
|
+
<AdminPageHeader
|
|
159
|
+
title="Categories"
|
|
160
|
+
subtitle="Organize your catalog into collections your customers browse."
|
|
161
|
+
actions={
|
|
162
|
+
<Link href="/admin/categories/new" className="admin-btn admin-btn-primary">
|
|
163
|
+
<Plus className="h-4 w-4" />
|
|
164
|
+
Add category
|
|
165
|
+
</Link>
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* Error state */}
|
|
170
|
+
{error && (
|
|
171
|
+
<div className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
172
|
+
{error}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Loading state */}
|
|
177
|
+
{isLoading && (
|
|
178
|
+
<div className="flex items-center justify-center py-12">
|
|
179
|
+
<span className="admin-spinner admin-spinner-lg" />
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Empty state */}
|
|
184
|
+
{!isLoading && !error && categoryTree.length === 0 && (
|
|
185
|
+
<div className="rounded-lg border border-border bg-card">
|
|
186
|
+
<AdminEmptyState
|
|
187
|
+
icon={<FolderOpen />}
|
|
188
|
+
title="No categories yet"
|
|
189
|
+
description="Get started by creating your first category."
|
|
190
|
+
action={
|
|
191
|
+
<Link
|
|
192
|
+
href="/admin/categories/new"
|
|
193
|
+
className="admin-btn admin-btn-primary"
|
|
194
|
+
>
|
|
195
|
+
<Plus className="h-4 w-4" />
|
|
196
|
+
Add category
|
|
197
|
+
</Link>
|
|
198
|
+
}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Category tree */}
|
|
204
|
+
{!isLoading && !error && categoryTree.length > 0 && (
|
|
205
|
+
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
|
206
|
+
{categoryTree.map(category => renderCategory(category, 0))}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Delete Confirmation Dialog */}
|
|
211
|
+
<DeleteConfirmDialog
|
|
212
|
+
isOpen={!!deleteTarget}
|
|
213
|
+
title="Delete Category"
|
|
214
|
+
itemName={deleteTarget?.name || ''}
|
|
215
|
+
itemType="category"
|
|
216
|
+
isDeleting={!!deletingId}
|
|
217
|
+
error={deleteError}
|
|
218
|
+
onConfirm={handleDeleteConfirm}
|
|
219
|
+
onCancel={handleDeleteCancel}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Customer Detail Page
|
|
5
|
+
*
|
|
6
|
+
* View and manage a single customer using the SDK CustomerDetails
|
|
7
|
+
* component, which renders the page header itself (back + eyebrow +
|
|
8
|
+
* name + verification badge) — LEDGER page architecture.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useParams, useRouter } from 'next/navigation'
|
|
12
|
+
import { CustomerDetails } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function CustomerDetailPage() {
|
|
15
|
+
const params = useParams()
|
|
16
|
+
const id = params.id as string
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<CustomerDetails
|
|
21
|
+
customerId={id}
|
|
22
|
+
onBack={() => router.push('/admin/customers')}
|
|
23
|
+
onDelete={() => router.push('/admin/customers')}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Customers Page
|
|
5
|
+
*
|
|
6
|
+
* Customer management view using SDK CustomerTable component.
|
|
7
|
+
* Displays ALL registered customers, not just those who ordered.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useRouter } from 'next/navigation'
|
|
11
|
+
import { AdminPageHeader, CustomerTable } from '@rovela-ai/sdk/admin'
|
|
12
|
+
|
|
13
|
+
export default function AdminCustomersPage() {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
|
|
16
|
+
const handleCustomerClick = (customerId: string) => {
|
|
17
|
+
router.push(`/admin/customers/${customerId}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-6">
|
|
22
|
+
<AdminPageHeader
|
|
23
|
+
title="Customers"
|
|
24
|
+
subtitle="Everyone with an account or an order in your store."
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<CustomerTable
|
|
28
|
+
pageSize={20}
|
|
29
|
+
onCustomerClick={handleCustomerClick}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Discounts Page
|
|
5
|
+
*
|
|
6
|
+
* The NATIVE discounts engine — discount codes and automatic promotions
|
|
7
|
+
* created and applied inside the store's own checkout. The DiscountsManager
|
|
8
|
+
* renders the page header itself (LEDGER page architecture): this wrapper
|
|
9
|
+
* stays a bare mount so the title and CTAs exist exactly once.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DiscountsManager } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function AdminDiscountsPage() {
|
|
15
|
+
return <DiscountsManager />
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Emails Page
|
|
5
|
+
*
|
|
6
|
+
* Every transactional email the store sends, on one shared brand theme.
|
|
7
|
+
* The EmailsManager renders the page header itself (LEDGER page
|
|
8
|
+
* architecture): this wrapper stays a bare mount so the title and CTAs
|
|
9
|
+
* exist exactly once.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EmailsManager } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function AdminEmailsPage() {
|
|
15
|
+
return <EmailsManager />
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Files Page
|
|
5
|
+
*
|
|
6
|
+
* The store's media library — every image and video under the store's R2
|
|
7
|
+
* prefix. Same live storage the Rovela platform's Assets Library lists.
|
|
8
|
+
* The FilesManager renders the page header itself (LEDGER page
|
|
9
|
+
* architecture): this wrapper stays a bare mount.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FilesManager } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function AdminFilesPage() {
|
|
15
|
+
return <FilesManager />
|
|
16
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Layout
|
|
3
|
+
*
|
|
4
|
+
* Protected admin area with sidebar navigation.
|
|
5
|
+
*
|
|
6
|
+
* Server-side gate: the layout is an async Server Component
|
|
7
|
+
* that calls `getCurrentAdmin()` and 307-redirects unauthenticated users to
|
|
8
|
+
* `/admin/login` BEFORE any HTML reaches the client. Eliminates the flash
|
|
9
|
+
* of admin chrome that previously appeared during the ~200–400ms session
|
|
10
|
+
* fetch window for unauthenticated visitors.
|
|
11
|
+
*
|
|
12
|
+
* Layered defense:
|
|
13
|
+
* - Server Component layout (this file): the primary gate. Stops cold
|
|
14
|
+
* loads of `/admin/*` from rendering any chrome for unauth visitors.
|
|
15
|
+
* - AdminGuard (client component, kept inside): catches mid-session
|
|
16
|
+
* expirations after the page is already rendered (e.g., JWT TTL
|
|
17
|
+
* elapses while the user is idle on /admin/products).
|
|
18
|
+
* - requireAdmin on every API route: the actual security boundary.
|
|
19
|
+
*
|
|
20
|
+
* AdminLayout is a client component (uses `rovela-admin-theme` chrome and
|
|
21
|
+
* the AdminNav with hooks) so it stays client-rendered — but it now only
|
|
22
|
+
* renders for authenticated users.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { redirect } from 'next/navigation'
|
|
26
|
+
import { AdminGuard, AdminLayout } from '@rovela-ai/sdk/admin'
|
|
27
|
+
import { getCurrentAdmin } from '@rovela-ai/sdk/admin/server'
|
|
28
|
+
|
|
29
|
+
export default async function AdminRootLayout({
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
}) {
|
|
34
|
+
const session = await getCurrentAdmin()
|
|
35
|
+
if (!session) {
|
|
36
|
+
redirect('/admin/login')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<AdminLayout>
|
|
41
|
+
<AdminGuard redirectTo="/admin/login">{children}</AdminGuard>
|
|
42
|
+
</AdminLayout>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Dashboard Loading State
|
|
3
|
+
*
|
|
4
|
+
* Shows skeleton UI during page transitions within the admin dashboard.
|
|
5
|
+
* Matches the dashboard layout structure for smooth visual continuity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default function AdminLoading() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="space-y-8">
|
|
11
|
+
{/* Welcome Header Skeleton */}
|
|
12
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
13
|
+
<div>
|
|
14
|
+
<div className="admin-skeleton h-8 w-64 rounded" />
|
|
15
|
+
<div className="admin-skeleton h-4 w-40 mt-2 rounded" />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="flex gap-2">
|
|
18
|
+
<div className="admin-skeleton h-10 w-28 rounded-lg" />
|
|
19
|
+
<div className="admin-skeleton h-10 w-28 rounded-lg" />
|
|
20
|
+
<div className="admin-skeleton h-10 w-10 rounded-lg" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* Stats Cards Skeleton */}
|
|
25
|
+
<section>
|
|
26
|
+
<div className="admin-skeleton h-4 w-20 mb-4 rounded" />
|
|
27
|
+
<div className="admin-stats-grid">
|
|
28
|
+
{[...Array(4)].map((_, i) => (
|
|
29
|
+
<div key={i} className="admin-card p-6">
|
|
30
|
+
<div className="admin-skeleton h-4 w-24 rounded" />
|
|
31
|
+
<div className="admin-skeleton h-9 w-32 mt-2 rounded" />
|
|
32
|
+
<div className="admin-skeleton h-3 w-20 mt-4 rounded" />
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
{/* Charts Skeleton */}
|
|
39
|
+
<section className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
40
|
+
<div className="xl:col-span-2 admin-card p-6">
|
|
41
|
+
<div className="admin-skeleton h-5 w-32 mb-4 rounded" />
|
|
42
|
+
<div className="admin-skeleton h-64 rounded" />
|
|
43
|
+
</div>
|
|
44
|
+
<div className="admin-card p-6">
|
|
45
|
+
<div className="admin-skeleton h-5 w-28 mb-4 rounded" />
|
|
46
|
+
<div className="admin-skeleton h-64 rounded" />
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
{/* Activity Section Skeleton */}
|
|
51
|
+
<section>
|
|
52
|
+
<div className="admin-skeleton h-4 w-16 mb-4 rounded" />
|
|
53
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
54
|
+
<div className="admin-card p-6">
|
|
55
|
+
<div className="admin-skeleton h-5 w-28 mb-4 rounded" />
|
|
56
|
+
<div className="space-y-3">
|
|
57
|
+
{[...Array(5)].map((_, i) => (
|
|
58
|
+
<div key={i} className="flex items-center gap-3">
|
|
59
|
+
<div className="admin-skeleton h-10 w-10 rounded" />
|
|
60
|
+
<div className="flex-1">
|
|
61
|
+
<div className="admin-skeleton h-4 w-32 rounded" />
|
|
62
|
+
<div className="admin-skeleton h-3 w-24 mt-1 rounded" />
|
|
63
|
+
</div>
|
|
64
|
+
<div className="admin-skeleton h-6 w-16 rounded" />
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="admin-card p-6">
|
|
70
|
+
<div className="admin-skeleton h-5 w-24 mb-4 rounded" />
|
|
71
|
+
<div className="space-y-3">
|
|
72
|
+
{[...Array(5)].map((_, i) => (
|
|
73
|
+
<div key={i} className="flex items-center gap-3">
|
|
74
|
+
<div className="admin-skeleton h-10 w-10 rounded" />
|
|
75
|
+
<div className="flex-1">
|
|
76
|
+
<div className="admin-skeleton h-4 w-28 rounded" />
|
|
77
|
+
<div className="admin-skeleton h-3 w-20 mt-1 rounded" />
|
|
78
|
+
</div>
|
|
79
|
+
<div className="admin-skeleton h-6 w-12 rounded" />
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Locations Page
|
|
5
|
+
*
|
|
6
|
+
* Physical shops (a standalone entity, Shopify's Locations model). Pickup
|
|
7
|
+
* enabled locations appear at checkout as "Pick up in store". The
|
|
8
|
+
* LocationsManager renders the page header itself (LEDGER page
|
|
9
|
+
* architecture): this wrapper stays a bare mount.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LocationsManager } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function AdminLocationsPage() {
|
|
15
|
+
return <LocationsManager />
|
|
16
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Order Detail Page
|
|
5
|
+
*
|
|
6
|
+
* View and manage individual order details.
|
|
7
|
+
* Uses SDK admin OrderDetails and RefundDialog components.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback } from 'react'
|
|
11
|
+
import { useParams, useRouter } from 'next/navigation'
|
|
12
|
+
import { OrderDetails, RefundDialog } from '@rovela-ai/sdk/admin'
|
|
13
|
+
|
|
14
|
+
export default function AdminOrderDetailPage() {
|
|
15
|
+
const params = useParams()
|
|
16
|
+
const id = params.id as string
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
|
|
19
|
+
// Refund dialog state
|
|
20
|
+
const [showRefundDialog, setShowRefundDialog] = useState(false)
|
|
21
|
+
const [refundOrderData, setRefundOrderData] = useState<{
|
|
22
|
+
orderId: string
|
|
23
|
+
orderTotal: string | number
|
|
24
|
+
orderCurrency?: string | null
|
|
25
|
+
} | null>(null)
|
|
26
|
+
|
|
27
|
+
// Handle refund button click from OrderDetails
|
|
28
|
+
const handleRefund = useCallback(
|
|
29
|
+
(orderId: string, orderTotal: string | number, orderCurrency?: string | null) => {
|
|
30
|
+
setRefundOrderData({ orderId, orderTotal, orderCurrency })
|
|
31
|
+
setShowRefundDialog(true)
|
|
32
|
+
},
|
|
33
|
+
[]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Handle refund success - refresh the page to show updated status
|
|
37
|
+
const handleRefundSuccess = useCallback(() => {
|
|
38
|
+
// Force a refresh by navigating to the same page
|
|
39
|
+
router.refresh()
|
|
40
|
+
}, [router])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="space-y-6">
|
|
44
|
+
<OrderDetails
|
|
45
|
+
orderId={id}
|
|
46
|
+
onBack={() => router.push('/admin/orders')}
|
|
47
|
+
onRefund={handleRefund}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
{/* Refund Dialog */}
|
|
51
|
+
{refundOrderData && (
|
|
52
|
+
<RefundDialog
|
|
53
|
+
orderId={refundOrderData.orderId}
|
|
54
|
+
orderTotal={refundOrderData.orderTotal}
|
|
55
|
+
currency={refundOrderData.orderCurrency}
|
|
56
|
+
isOpen={showRefundDialog}
|
|
57
|
+
onClose={() => {
|
|
58
|
+
setShowRefundDialog(false)
|
|
59
|
+
setRefundOrderData(null)
|
|
60
|
+
}}
|
|
61
|
+
onSuccess={handleRefundSuccess}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Orders Page
|
|
5
|
+
*
|
|
6
|
+
* Order management with table and filters.
|
|
7
|
+
* Uses SDK admin OrderTable component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useRouter } from 'next/navigation'
|
|
11
|
+
import { AdminPageHeader, OrderTable } from '@rovela-ai/sdk/admin'
|
|
12
|
+
|
|
13
|
+
export default function AdminOrdersPage() {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="space-y-6">
|
|
18
|
+
<AdminPageHeader
|
|
19
|
+
title="Orders"
|
|
20
|
+
subtitle="Track, fulfill and refund your customers' orders."
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<OrderTable onOrderClick={(id) => router.push(`/admin/orders/${id}`)} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|