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
|
||||
</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>
|
||||
|
||||
@ -124,52 +124,20 @@ export default function QuickActionDashboardPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative min-h-screen w-full px-3 sm:px-4 py-10">
|
||||
{/* Background Pattern */}
|
||||
<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="relative min-h-screen w-full px-3 sm:px-4 py-10 bg-white">
|
||||
{/* Removed dark background layers (pattern, blur, gradient) for a clean white background */}
|
||||
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Welcome */}
|
||||
<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}` : ''}!
|
||||
</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'}
|
||||
</p>
|
||||
{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 && (
|
||||
<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