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)
234 lines
9.3 KiB
TypeScript
234 lines
9.3 KiB
TypeScript
import Link from 'next/link';
|
|
import {
|
|
Briefcase,
|
|
Users,
|
|
TrendingUp,
|
|
Target,
|
|
ArrowRight,
|
|
} from 'lucide-react';
|
|
import { Badge, Card, CardBody, Avatar } from '@nextcraft/ui';
|
|
import { candidates, employers } from '@nextcraft/mock-data';
|
|
import { AnalyticsCharts } from '../../../components/employer/analytics-charts';
|
|
import { matchBadgeClasses, initials } from '../../../lib/format';
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Mock pipeline + metric data */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
const METRICS = [
|
|
{ label: 'Active Postings', value: 7, trend: '+12%', icon: Briefcase, tone: 'primary' as const },
|
|
{ label: 'Total Applicants', value: 142, trend: '+24%', icon: Users, tone: 'accent' as const },
|
|
{ label: 'Talent Matches', value: 28, trend: '+8%', icon: Target, tone: 'violet' as const },
|
|
{ label: 'Placement Rate', value: '18%', trend: '+3%', icon: TrendingUp, tone: 'amber' as const },
|
|
];
|
|
|
|
const TONE_CLASSES: Record<string, string> = {
|
|
primary: 'bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300',
|
|
accent: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
|
violet: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300',
|
|
amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
|
};
|
|
|
|
interface PipelineStage {
|
|
name: string;
|
|
count: number;
|
|
cards: Array<{ name: string; position: string; score: number }>;
|
|
}
|
|
|
|
const PIPELINE: PipelineStage[] = [
|
|
{
|
|
name: 'Applied',
|
|
count: 8,
|
|
cards: [
|
|
{ name: 'Maya Okonkwo', position: 'AI Orchestration Engineer', score: 96 },
|
|
{ name: 'Devon Park', position: 'AI Safety Researcher', score: 91 },
|
|
{ name: 'Priya Iyer', position: 'Human-AI Designer', score: 89 },
|
|
],
|
|
},
|
|
{
|
|
name: 'Screening',
|
|
count: 5,
|
|
cards: [
|
|
{ name: 'Tomás Vega', position: 'LLM App Developer', score: 93 },
|
|
{ name: 'Sofia Marchetti', position: 'AI Governance Lead', score: 85 },
|
|
],
|
|
},
|
|
{
|
|
name: 'Interview',
|
|
count: 3,
|
|
cards: [
|
|
{ name: 'Liam Chen', position: 'Agent Reliability Eng', score: 90 },
|
|
{ name: 'Yuki Tanaka', position: 'Evaluation Engineer', score: 86 },
|
|
],
|
|
},
|
|
{
|
|
name: 'Offer',
|
|
count: 2,
|
|
cards: [{ name: 'Ingrid Solberg', position: 'AI Red Team Lead', score: 88 }],
|
|
},
|
|
{
|
|
name: 'Hired',
|
|
count: 1,
|
|
cards: [{ name: 'Hana Lindqvist', position: 'Comp Biologist', score: 88 }],
|
|
},
|
|
];
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Page */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export default function EmployerDashboardPage() {
|
|
const employer = employers[0];
|
|
const topMatches = [...candidates].sort((a, b) => b.matchScore - a.matchScore).slice(0, 5);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-8">
|
|
{/* Header */}
|
|
<header className="flex flex-col gap-2">
|
|
<Badge variant="info">Employer Dashboard</Badge>
|
|
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
|
|
Employer Dashboard
|
|
</h1>
|
|
<p className="text-slate-600 dark:text-slate-400">
|
|
Welcome back, <span className="font-semibold text-slate-800 dark:text-slate-200">{employer.name}</span>.
|
|
Here is your talent pipeline at a glance.
|
|
</p>
|
|
</header>
|
|
|
|
{/* Metric cards */}
|
|
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
{METRICS.map((m) => {
|
|
const Icon = m.icon;
|
|
return (
|
|
<Card key={m.label}>
|
|
<CardBody className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between">
|
|
<span
|
|
className={`inline-flex h-10 w-10 items-center justify-center rounded-full ${TONE_CLASSES[m.tone]}`}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
</span>
|
|
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
|
{m.trend}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{m.value}
|
|
</span>
|
|
<span className="text-sm text-slate-500 dark:text-slate-400">{m.label}</span>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
{/* Middle row — pipeline + talent matches */}
|
|
<section className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Applicant pipeline — 2/3 width */}
|
|
<div className="lg:col-span-2">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
Applicant Pipeline
|
|
</h2>
|
|
<span className="text-sm text-slate-500 dark:text-slate-400">
|
|
{PIPELINE.reduce((s, c) => s + c.count, 0)} candidates in flight
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
{PIPELINE.map((stage) => (
|
|
<div
|
|
key={stage.name}
|
|
className="flex w-56 shrink-0 flex-col gap-2 rounded-lg border border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900/60"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">
|
|
{stage.name}
|
|
</h3>
|
|
<span className="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-slate-200 px-1.5 text-xs font-medium text-slate-700 dark:bg-slate-700 dark:text-slate-200">
|
|
{stage.count}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
{stage.cards.map((c) => (
|
|
<div
|
|
key={c.name}
|
|
className="flex items-center gap-2 rounded-md border border-slate-200 bg-white p-2 dark:border-slate-800 dark:bg-slate-900"
|
|
>
|
|
<Avatar name={c.name} size="sm" />
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-xs font-medium text-slate-800 dark:text-slate-100">
|
|
{c.name}
|
|
</span>
|
|
<span className="truncate text-xs text-slate-500 dark:text-slate-400">
|
|
{c.position}
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={`inline-flex h-7 w-9 shrink-0 items-center justify-center rounded-full text-xs font-bold ring-2 ${matchBadgeClasses(
|
|
c.score,
|
|
)}`}
|
|
>
|
|
{c.score}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Talent matches — 1/3 width */}
|
|
<div>
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
Talent Matches
|
|
</h2>
|
|
<Link
|
|
href="/employer/talent"
|
|
className="inline-flex items-center gap-1 text-sm font-medium text-primary-700 hover:underline dark:text-primary-300"
|
|
>
|
|
See all <ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</div>
|
|
<Card>
|
|
<CardBody className="flex flex-col gap-2 p-3">
|
|
{topMatches.map((cand) => (
|
|
<Link
|
|
key={cand.id}
|
|
href={`/employer/talent/${cand.id}`}
|
|
className="flex items-center gap-3 rounded-md p-2 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800"
|
|
>
|
|
<Avatar name={cand.name} src={cand.avatar} size="md" />
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-sm font-medium text-slate-800 dark:text-slate-100">
|
|
{cand.name}
|
|
</span>
|
|
<span className="truncate text-xs text-slate-500 dark:text-slate-400">
|
|
{cand.headline}
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={`inline-flex h-8 w-12 shrink-0 items-center justify-center rounded-full text-xs font-bold ring-2 ${matchBadgeClasses(
|
|
cand.matchScore,
|
|
)}`}
|
|
>
|
|
{cand.matchScore}%
|
|
</span>
|
|
</Link>
|
|
))}
|
|
</CardBody>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Analytics charts */}
|
|
<section>
|
|
<h2 className="mb-3 text-lg font-semibold text-slate-900 dark:text-slate-100">Analytics</h2>
|
|
<AnalyticsCharts />
|
|
</section>
|
|
</div>
|
|
);
|
|
} |