810485ecc8
---ci--- phase: 7 milestone: v0.1 status: complete requirements: covered: [REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, REQ-011, REQ-012, REQ-013, REQ-014, REQ-015, REQ-016, REQ-017, REQ-018, REQ-019, REQ-020, REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026, REQ-027, REQ-028] partial: [] ---/ci--- Milestone v0.1 (nextcraft-ui-prototype) complete. Summary: - 7 phases (P0 pre-execution + P1-P6 execution + P7 final review) - 28 requirements covered (all complete) - 4 surfaces: Learner (7 pages), Marketplace (5), Employer Dashboard (4), Admin (4) - 19 routes, 104 TypeScript files - Shared component library (5 primitives + design tokens) - Mock data: 5 competency stacks (70 competencies), 20 jobs, 15 candidates, 10 employers - Tech: Next.js 15, Tailwind CSS v4, lucide-react, recharts, @xyflow/react, Inter font - Storybook with 6 stories - Dark mode, breadcrumbs, role switcher, responsive design - Build passes, typecheck passes, Storybook build passes Phases: P0 pre-execution → v0.0.1 P1 project-scaffolding → v0.0.2 P2 learner-surface → v0.0.3 P3 marketplace-surface → v0.0.4 P4 employer-dashboard → v0.0.5 P5 admin-surface → v0.0.6 P6 polish-integration → v0.0.7 P7 final-review-ship → v0.1.0 (milestone release)
244 lines
9.2 KiB
TypeScript
244 lines
9.2 KiB
TypeScript
import {
|
|
Activity,
|
|
AlertTriangle,
|
|
Briefcase,
|
|
Building2,
|
|
CheckCircle,
|
|
Clock,
|
|
Cpu,
|
|
Database,
|
|
GraduationCap,
|
|
Server,
|
|
ThumbsUp,
|
|
Users,
|
|
} from 'lucide-react';
|
|
import type { LucideIcon } from 'lucide-react';
|
|
import { Card, CardBody, CardHeader } from '@nextcraft/ui';
|
|
import {
|
|
activityFeed,
|
|
platformMetrics,
|
|
serviceHealth,
|
|
systemStats,
|
|
uptimeBars,
|
|
} from '@nextcraft/mock-data';
|
|
|
|
const ICONS: Record<string, LucideIcon> = {
|
|
Users,
|
|
Building2,
|
|
Briefcase,
|
|
GraduationCap,
|
|
ThumbsUp,
|
|
Activity,
|
|
Server,
|
|
Database,
|
|
Cpu,
|
|
};
|
|
|
|
const TONE_CLASSES: Record<string, string> = {
|
|
indigo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
|
|
emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
|
amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
|
rose: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300',
|
|
cyan: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-300',
|
|
violet: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300',
|
|
};
|
|
|
|
function statusDot(status: string): string {
|
|
if (status === 'operational') return 'bg-emerald-500';
|
|
if (status === 'degraded') return 'bg-amber-500';
|
|
return 'bg-rose-500';
|
|
}
|
|
|
|
function statusLabel(status: string): string {
|
|
if (status === 'operational') return 'Operational';
|
|
if (status === 'degraded') return 'Degraded';
|
|
return 'Down';
|
|
}
|
|
|
|
export default function AdminOverviewPage() {
|
|
return (
|
|
<div className="flex flex-col gap-8">
|
|
{/* Page header */}
|
|
<header className="flex flex-col gap-1">
|
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
Admin Dashboard
|
|
</h1>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
|
Platform overview
|
|
</p>
|
|
</header>
|
|
|
|
{/* Metric cards */}
|
|
<section
|
|
aria-label="Platform metrics"
|
|
className="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-5"
|
|
>
|
|
{platformMetrics.map((m) => {
|
|
const Icon = ICONS[m.icon] ?? Activity;
|
|
const positive = m.trendPct >= 0;
|
|
return (
|
|
<Card key={m.id}>
|
|
<CardBody className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between">
|
|
<span
|
|
className={`inline-flex h-9 w-9 items-center justify-center rounded-full ${TONE_CLASSES[m.tone]}`}
|
|
aria-hidden
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
</span>
|
|
<span
|
|
className={`text-xs font-medium ${positive ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'}`}
|
|
>
|
|
{positive ? '+' : ''}
|
|
{m.trendPct}%
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
{m.label}
|
|
</p>
|
|
<p className="mt-0.5 text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{m.value.toLocaleString()}
|
|
{m.suffix}
|
|
</p>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
{/* Activity + System health */}
|
|
<section className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Activity feed — 2/3 width */}
|
|
<div className="lg:col-span-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Activity className="h-4 w-4 text-primary-600" aria-hidden />
|
|
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
|
|
Activity Feed
|
|
</h2>
|
|
</div>
|
|
</CardHeader>
|
|
<CardBody className="p-0">
|
|
<ol className="flex flex-col">
|
|
{activityFeed.map((evt, i) => {
|
|
const Icon = ICONS[evt.icon] ?? Activity;
|
|
const last = i === activityFeed.length - 1;
|
|
return (
|
|
<li
|
|
key={evt.id}
|
|
className={`flex items-start gap-3 px-6 py-3 ${last ? '' : 'border-b border-slate-100 dark:border-slate-800'}`}
|
|
>
|
|
<span
|
|
className={`mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${TONE_CLASSES[evt.tone]}`}
|
|
aria-hidden
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
</span>
|
|
<div className="flex flex-1 flex-col gap-0.5">
|
|
<p className="text-sm text-slate-700 dark:text-slate-200">
|
|
{evt.message}
|
|
</p>
|
|
<span className="flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
|
|
<Clock className="h-3 w-3" aria-hidden />
|
|
{evt.relativeTime}
|
|
</span>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</CardBody>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* System health — 1/3 width */}
|
|
<div>
|
|
<Card className="h-full">
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Server className="h-4 w-4 text-primary-600" aria-hidden />
|
|
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
|
|
System Health
|
|
</h2>
|
|
</div>
|
|
</CardHeader>
|
|
<CardBody className="flex flex-col gap-5">
|
|
<ul className="flex flex-col gap-2.5">
|
|
{serviceHealth.map((svc) => (
|
|
<li
|
|
key={svc.id}
|
|
className="flex items-center justify-between gap-2"
|
|
>
|
|
<span className="text-sm text-slate-700 dark:text-slate-200">
|
|
{svc.name}
|
|
</span>
|
|
<span className="flex items-center gap-2">
|
|
<span
|
|
className={`h-2.5 w-2.5 rounded-full ${statusDot(svc.status)}`}
|
|
aria-hidden
|
|
/>
|
|
<span
|
|
className={`text-xs font-medium ${svc.status === 'operational' ? 'text-emerald-600 dark:text-emerald-400' : svc.status === 'degraded' ? 'text-amber-600 dark:text-amber-400' : 'text-rose-600 dark:text-rose-400'}`}
|
|
>
|
|
{statusLabel(svc.status)}
|
|
</span>
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
Uptime (30d)
|
|
</h3>
|
|
<span className="text-xs font-medium text-slate-600 dark:text-slate-300">
|
|
99.96%
|
|
</span>
|
|
</div>
|
|
<div className="flex h-8 items-end gap-0.5" aria-hidden>
|
|
{uptimeBars.map((u, idx) => {
|
|
const h = Math.max(20, Math.round((u - 99.8) * 100 * 4));
|
|
const degraded = u < 99.95;
|
|
return (
|
|
<span
|
|
key={idx}
|
|
className={`flex-1 rounded-sm ${degraded ? 'bg-amber-400' : 'bg-emerald-400'}`}
|
|
style={{ height: `${Math.min(100, h)}%` }}
|
|
title={`${u.toFixed(2)}%`}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3 border-t border-slate-200 pt-4 dark:border-slate-800">
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
Error Rate
|
|
</span>
|
|
<span className="flex items-center gap-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" aria-hidden />
|
|
{systemStats.errorRate}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
Avg Response Time
|
|
</span>
|
|
<span className="flex items-center gap-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" aria-hidden />
|
|
{systemStats.avgResponseMs}ms
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
} |