Files
nextcraft/apps/web/app/(admin)/admin/page.tsx
T
CIAgent 810485ecc8 docs(milestone): complete v0.1-nextcraft-ui-prototype
---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)
2026-09-10 22:47:56 +00:00

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