feat: add frontend matrix management
This commit is contained in:
parent
fb82536a09
commit
d561e7da82
306
src/app/admin/matrix-management/page.tsx
Normal file
306
src/app/admin/matrix-management/page.tsx
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useMemo, useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
ChartBarIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
UsersIcon,
|
||||||
|
PlusIcon,
|
||||||
|
EnvelopeIcon,
|
||||||
|
CalendarDaysIcon,
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
import PageLayout from '../../components/PageLayout'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import useAuthStore from '../../store/authStore'
|
||||||
|
|
||||||
|
type Matrix = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: 'active' | 'inactive'
|
||||||
|
usersCount: number
|
||||||
|
createdAt: string
|
||||||
|
topNodeEmail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MatrixManagementPage() {
|
||||||
|
// Auth guard
|
||||||
|
const router = useRouter()
|
||||||
|
const user = useAuthStore(s => s.user)
|
||||||
|
const isAdmin =
|
||||||
|
!!user &&
|
||||||
|
(
|
||||||
|
(user as any)?.role === 'admin' ||
|
||||||
|
(user as any)?.userType === 'admin' ||
|
||||||
|
(user as any)?.isAdmin === true ||
|
||||||
|
((user as any)?.roles?.includes?.('admin'))
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user === null) {
|
||||||
|
router.push('/login')
|
||||||
|
} else if (user && !isAdmin) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}, [user, isAdmin, router])
|
||||||
|
|
||||||
|
const [matrices, setMatrices] = useState<Matrix[]>([
|
||||||
|
{
|
||||||
|
id: 'm1',
|
||||||
|
name: 'Gold Matrix',
|
||||||
|
status: 'active',
|
||||||
|
usersCount: 128,
|
||||||
|
createdAt: new Date(Date.now() - 5 * 24 * 3600 * 1000).toISOString(),
|
||||||
|
topNodeEmail: 'alice@example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm2',
|
||||||
|
name: 'Silver Matrix',
|
||||||
|
status: 'inactive',
|
||||||
|
usersCount: 64,
|
||||||
|
createdAt: new Date(Date.now() - 15 * 24 * 3600 * 1000).toISOString(),
|
||||||
|
topNodeEmail: 'bob@example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm3',
|
||||||
|
name: 'Bronze Matrix',
|
||||||
|
status: 'active',
|
||||||
|
usersCount: 42,
|
||||||
|
createdAt: new Date(Date.now() - 40 * 24 * 3600 * 1000).toISOString(),
|
||||||
|
topNodeEmail: 'charlie@example.com',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [createName, setCreateName] = useState('')
|
||||||
|
const [createEmail, setCreateEmail] = useState('')
|
||||||
|
const [formError, setFormError] = useState<string>('')
|
||||||
|
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = matrices.length
|
||||||
|
const active = matrices.filter(m => m.status === 'active').length
|
||||||
|
const totalUsers = matrices.reduce((acc, m) => acc + (m.usersCount || 0), 0)
|
||||||
|
return { total, active, totalUsers }
|
||||||
|
}, [matrices])
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setCreateName('')
|
||||||
|
setCreateEmail('')
|
||||||
|
setFormError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateEmail = (email: string) =>
|
||||||
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())
|
||||||
|
|
||||||
|
const handleCreate = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const name = createName.trim()
|
||||||
|
const email = createEmail.trim()
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
setFormError('Please provide a matrix name.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!email || !validateEmail(email)) {
|
||||||
|
setFormError('Please provide a valid top-node email.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMatrix: Matrix = {
|
||||||
|
id: `m-${Date.now()}`,
|
||||||
|
name,
|
||||||
|
status: 'active',
|
||||||
|
usersCount: 0,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
topNodeEmail: email,
|
||||||
|
}
|
||||||
|
setMatrices(prev => [newMatrix, ...prev])
|
||||||
|
setCreateOpen(false)
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleStatus = (id: string) => {
|
||||||
|
setMatrices(prev =>
|
||||||
|
prev.map(m => (m.id === id ? { ...m, status: m.status === 'active' ? 'inactive' : 'active' } : m))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatCard = ({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
icon: any
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
color: string
|
||||||
|
}) => (
|
||||||
|
<div className="relative overflow-hidden rounded-lg bg-white px-4 pb-6 pt-5 shadow-sm border border-gray-200 sm:px-6 sm:pt-6">
|
||||||
|
<dt>
|
||||||
|
<div className={`absolute rounded-md ${color} p-3`}>
|
||||||
|
<Icon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<p className="ml-16 truncate text-sm font-medium text-gray-500">{label}</p>
|
||||||
|
</dt>
|
||||||
|
<dd className="ml-16 mt-2 text-2xl font-semibold text-gray-900">{value}</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }: { status: Matrix['status'] }) => (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
||||||
|
status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`mr-1.5 h-1.5 w-1.5 rounded-full ${
|
||||||
|
status === 'active' ? 'bg-green-500' : 'bg-gray-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<div className="min-h-screen w-full px-4 sm:px-6 py-8 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header + Create */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Matrix Management</h1>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">Manage matrices, see stats, and create new ones.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
Create New Matrix
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 mb-8">
|
||||||
|
<StatCard icon={CheckCircleIcon} label="Active Matrices" value={stats.active} color="bg-green-500" />
|
||||||
|
<StatCard icon={ChartBarIcon} label="Total Matrices" value={stats.total} color="bg-indigo-500" />
|
||||||
|
<StatCard icon={UsersIcon} label="Total Users Subscribed" value={stats.totalUsers} color="bg-amber-600" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Matrix cards */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{matrices.map(m => (
|
||||||
|
<article key={m.id} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{m.name}</h3>
|
||||||
|
<StatusBadge status={m.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-3 text-sm text-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UsersIcon className="h-5 w-5 text-gray-500" />
|
||||||
|
<span className="font-medium">{m.usersCount}</span>
|
||||||
|
<span className="text-gray-500">users</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarDaysIcon className="h-5 w-5 text-gray-500" />
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{new Date(m.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<EnvelopeIcon className="h-5 w-5 text-gray-500" />
|
||||||
|
<span className="text-gray-700 truncate">{m.topNodeEmail}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleStatus(m.id)}
|
||||||
|
className={`rounded-md px-3 py-2 text-sm font-medium border ${
|
||||||
|
m.status === 'active'
|
||||||
|
? 'border-red-300 text-red-700 hover:bg-red-50'
|
||||||
|
: 'border-green-300 text-green-700 hover:bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m.status === 'active' ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-sm font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
onClick={() => alert('Placeholder: open matrix details')}
|
||||||
|
>
|
||||||
|
View details →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Matrix Modal */}
|
||||||
|
{createOpen && (
|
||||||
|
<div className="fixed inset-0 z-50">
|
||||||
|
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setCreateOpen(false)} />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md rounded-xl bg-white shadow-2xl ring-1 ring-black/10">
|
||||||
|
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||||
|
<h4 className="text-base font-semibold text-gray-900">Create New Matrix</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => { setCreateOpen(false); resetForm() }}
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleCreate} className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-800 mb-1">Matrix Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={createName}
|
||||||
|
onChange={e => setCreateName(e.target.value)}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="e.g., Platinum Matrix"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-800 mb-1">Top-node Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={createEmail}
|
||||||
|
onChange={e => setCreateEmail(e.target.value)}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="owner@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{formError && (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||||
|
{formError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="pt-2 flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setCreateOpen(false); resetForm() }}
|
||||||
|
className="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Create Matrix
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -411,6 +411,13 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
User Verify
|
User Verify
|
||||||
</button>
|
</button>
|
||||||
|
{/* NEW: Matrix Management link */}
|
||||||
|
<button
|
||||||
|
onClick={() => { console.log('🧭 Admin: navigate to /admin/matrix-management'); router.push('/admin/matrix-management') }}
|
||||||
|
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
|
||||||
|
>
|
||||||
|
Matrix Management
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -124,52 +124,20 @@ export default function QuickActionDashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen w-full px-3 sm:px-4 py-10">
|
<div className="relative min-h-screen w-full px-3 sm:px-4 py-10 bg-white">
|
||||||
{/* Background Pattern */}
|
{/* Removed dark background layers (pattern, blur, gradient) for a clean white background */}
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
x="50%"
|
|
||||||
y={-1}
|
|
||||||
id="affiliate-pattern"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#affiliate-pattern)" width="100%" height="100%" strokeWidth={0} />
|
|
||||||
</svg>
|
|
||||||
{/* Colored Blur Effect */}
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
clipPath:
|
|
||||||
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)',
|
|
||||||
}}
|
|
||||||
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Gradient base */}
|
|
||||||
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* Welcome */}
|
{/* Welcome */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
|
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 tracking-tight">
|
||||||
Welcome{isClient && user?.firstName ? `, ${user.firstName}` : ''}!
|
Welcome{isClient && user?.firstName ? `, ${user.firstName}` : ''}!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm sm:text-base text-blue-100 font-medium mt-2">
|
<p className="text-sm sm:text-base text-gray-600 font-medium mt-2">
|
||||||
{isClient && user?.userType === 'company' ? 'Company Account' : 'Personal Account'}
|
{isClient && user?.userType === 'company' ? 'Company Account' : 'Personal Account'}
|
||||||
</p>
|
</p>
|
||||||
{loading && (
|
{loading && (
|
||||||
<p className="text-xs text-blue-200 mt-1">Loading status...</p>
|
<p className="text-xs text-gray-500 mt-1">Loading status...</p>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-4 max-w-md mx-auto rounded-md bg-red-50 border border-red-200 px-4 py-3">
|
<div className="mt-4 max-w-md mx-auto rounded-md bg-red-50 border border-red-200 px-4 py-3">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user