From ffb1fafc7ebdf24e96b4b8fe855f57ee7808c638 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Sat, 4 Oct 2025 00:48:22 +0200 Subject: [PATCH] feature: add admin user-management page --- src/app/admin/user-management/page.tsx | 340 +++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 src/app/admin/user-management/page.tsx diff --git a/src/app/admin/user-management/page.tsx b/src/app/admin/user-management/page.tsx new file mode 100644 index 0000000..9091033 --- /dev/null +++ b/src/app/admin/user-management/page.tsx @@ -0,0 +1,340 @@ +'use client' + +import { useMemo, useState } from 'react' +import PageLayout from '../../components/PageLayout' +import { + MagnifyingGlassIcon, + EyeIcon, + PencilSquareIcon, + XMarkIcon +} from '@heroicons/react/24/outline' + +type UserType = 'personal' | 'company' +type UserStatus = 'active' | 'pending' | 'disabled' +type UserRole = 'user' | 'admin' + +interface User { + id: string + firstName: string + lastName: string + email: string + type: UserType + status: UserStatus + role: UserRole + created: string + lastLogin: string | null +} + +const STATUSES: UserStatus[] = ['active','pending','disabled'] +const TYPES: UserType[] = ['personal','company'] +const ROLES: UserRole[] = ['user','admin'] + +// Helpers +const rand = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)] +const daysAgo = (d: number) => { + const dt = new Date() + dt.setDate(dt.getDate() - d) + return dt.toISOString().slice(0,10) +} + +export default function AdminUserManagementPage() { + // Mock users (stable memo) + const users = useMemo(() => { + const firstNames = ['Anka','Peter','Primoz','Bojana','Hiska','Augst','Katarina','Tamara','Darija','Luka','Sara','Jonas','Maja','Niko','Eva'] + const lastNames = ['Eravec','Oblak','Sranic','Pilih','Kaja','Feviz','Kravar','Skusek','Abersek','Novak','Schmidt','Mueller','Keller','Hansen','Mayr'] + const list: User[] = [] + for (let i=0;i<101;i++){ + const fn = rand(firstNames) + const ln = rand(lastNames) + const createdDays = Math.floor(Math.random()*15) + const loginDays = Math.random() > 0.2 ? Math.floor(Math.random()*15) : null + list.push({ + id: `U${i+1}`, + firstName: fn, + lastName: ln, + email: `${fn}.${ln}${i}@example.com`.toLowerCase(), + type: Math.random() > 0.92 ? 'company' : 'personal', + status: rand(STATUSES), + role: Math.random() > 0.96 ? 'admin' : 'user', + created: daysAgo(createdDays), + lastLogin: loginDays === null ? null : daysAgo(loginDays) + }) + } + return list + }, []) + + // Filters + const [search, setSearch] = useState('') + const [fType, setFType] = useState<'all'|UserType>('all') + const [fStatus, setFStatus] = useState<'all'|UserStatus>('all') + const [fRole, setFRole] = useState<'all'|UserRole>('all') + const [page, setPage] = useState(1) + const PAGE_SIZE = 10 + + const filtered = useMemo(() => { + return users.filter(u => + (fType==='all'||u.type===fType) && + (fStatus==='all'||u.status===fStatus) && + (fRole==='all'||u.role===fRole) && + ( + !search.trim() || + u.email.toLowerCase().includes(search.toLowerCase()) || + `${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase()) + ) + ) + }, [users, search, fType, fStatus, fRole]) + + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) + const current = filtered.slice((page-1)*PAGE_SIZE, page*PAGE_SIZE) + + const applyFilter = (e: React.FormEvent) => { + e.preventDefault() + setPage(1) + } + + const badge = (text: string, color: 'blue'|'amber'|'green'|'gray'|'rose'|'indigo'|'purple') => { + const base = 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium tracking-wide' + const map: Record = { + blue: 'bg-blue-100 text-blue-700', + amber: 'bg-amber-100 text-amber-700', + green: 'bg-green-100 text-green-700', + gray: 'bg-gray-100 text-gray-700', + rose: 'bg-rose-100 text-rose-700', + indigo: 'bg-indigo-100 text-indigo-700', + purple: 'bg-purple-100 text-purple-700' + } + return {text} + } + + const statusBadge = (s: UserStatus) => + s==='active' ? badge('Active','green') + : s==='pending' ? badge('Pending','amber') + : badge('Disabled','rose') + + const typeBadge = (t: UserType) => + t==='personal' ? badge('Personal','blue') : badge('Company','purple') + + const roleBadge = (r: UserRole) => + r==='admin' ? badge('Admin','indigo') : badge('User','gray') + + // Action stubs + const onView = (id: string) => console.log('View', id) + const onEdit = (id: string) => console.log('Edit', id) + const onDelete = (id: string) => console.log('Delete', id) + + return ( + +
+ {/* Background */} +
+
+ + + + {/* Outer wrapper card */} +
+
+

+ User Management +

+

+ Manage all users, view statistics, and handle verification. +

+
+ + {/* Filter Card */} +
+

+ Search & Filter Users +

+
+ {/* Search */} +
+ +
+ + setSearch(e.target.value)} + placeholder="Email, name, company..." + className="w-full rounded-md border border-gray-300 pl-10 pr-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+
+ {/* Type */} +
+ +
+ {/* Status */} +
+ +
+ {/* Role */} +
+ +
+
+
+ +
+
+ + {/* Users Table */} +
+
+
+ All Users +
+
+ Showing {current.length} of {filtered.length} users +
+
+
+ + + + + + + + + + + + + + {current.map(u => { + const initials = `${u.firstName[0]||''}${u.lastName[0]||''}`.toUpperCase() + return ( + + + + + + + + + + ) + })} + {current.length === 0 && ( + + + + )} + +
UserTypeStatusRoleCreatedLast LoginActions
+
+
+ {initials} +
+
+
+ {u.firstName} {u.lastName} +
+
+ {u.email} +
+
+
+
{typeBadge(u.type)}{statusBadge(u.status)}{roleBadge(u.role)}{u.created} + {u.lastLogin ?? 'Never'} + +
+ + + +
+
+ No users match current filters. +
+
+ {/* Pagination */} +
+
+ Page {page} of {totalPages} ({filtered.length} total users) +
+
+ + +
+
+
+
+
+ + ) +}