feat: add frontend matrix management

This commit is contained in:
DeathKaioken 2025-10-16 10:09:31 +02:00
parent fb82536a09
commit d561e7da82
3 changed files with 319 additions and 38 deletions

View 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>
)
}

View File

@ -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>

View File

@ -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">