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)
157 lines
6.1 KiB
TypeScript
157 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
Cell,
|
|
Legend,
|
|
Line,
|
|
LineChart,
|
|
Pie,
|
|
PieChart,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import { colors } from '@nextcraft/ui';
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Mock chart data */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
const postingsData = [
|
|
{ month: 'Apr', postings: 4, applications: 38 },
|
|
{ month: 'May', postings: 6, applications: 62 },
|
|
{ month: 'Jun', postings: 5, applications: 51 },
|
|
{ month: 'Jul', postings: 8, applications: 84 },
|
|
{ month: 'Aug', postings: 9, applications: 112 },
|
|
{ month: 'Sep', postings: 7, applications: 142 },
|
|
];
|
|
|
|
const sourceData = [
|
|
{ name: 'Direct', value: 40, color: colors.primary[500] },
|
|
{ name: 'AI Match', value: 30, color: colors.accent[500] },
|
|
{ name: 'Referral', value: 20, color: colors.violet[400] },
|
|
{ name: 'Job Board', value: 10, color: colors.amber[400] },
|
|
];
|
|
|
|
const placementData = [
|
|
{ month: 'Apr', placements: 2 },
|
|
{ month: 'May', placements: 5 },
|
|
{ month: 'Jun', placements: 9 },
|
|
{ month: 'Jul', placements: 14 },
|
|
{ month: 'Aug', placements: 21 },
|
|
{ month: 'Sep', placements: 28 },
|
|
];
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Shared styling */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
const tooltipStyle = {
|
|
borderRadius: 8,
|
|
border: `1px solid ${colors.neutral[200]}`,
|
|
fontSize: 12,
|
|
background: '#ffffff',
|
|
} as const;
|
|
|
|
const axisTick = { fontSize: 12, fill: colors.neutral[500] } as const;
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Component */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export function AnalyticsCharts() {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Bar chart — Postings vs Applications */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
|
<h3 className="mb-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
Postings vs Applications
|
|
</h3>
|
|
<p className="mb-4 text-xs text-slate-500 dark:text-slate-400">Last 6 months</p>
|
|
<div className="h-56 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={postingsData} margin={{ top: 4, right: 8, left: -16, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke={colors.neutral[200]} className="dark:opacity-20" />
|
|
<XAxis dataKey="month" tick={axisTick} stroke={colors.neutral[300]} />
|
|
<YAxis tick={axisTick} stroke={colors.neutral[300]} />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
cursor={{ fill: 'rgba(99,102,241,0.06)' }}
|
|
/>
|
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
<Bar dataKey="postings" fill={colors.primary[500]} radius={[4, 4, 0, 0]} name="Postings" />
|
|
<Bar dataKey="applications" fill={colors.accent[500]} radius={[4, 4, 0, 0]} name="Applications" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Donut chart — Applicant Sources */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
|
<h3 className="mb-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
Applicant Sources
|
|
</h3>
|
|
<p className="mb-4 text-xs text-slate-500 dark:text-slate-400">Where candidates come from</p>
|
|
<div className="h-56 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={sourceData}
|
|
dataKey="value"
|
|
nameKey="name"
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={50}
|
|
outerRadius={80}
|
|
paddingAngle={3}
|
|
>
|
|
{sourceData.map((entry) => (
|
|
<Cell key={entry.name} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip contentStyle={tooltipStyle} formatter={(v: number) => `${v}%`} />
|
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Line chart — Placement Trends */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
|
<h3 className="mb-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
Placement Trends
|
|
</h3>
|
|
<p className="mb-4 text-xs text-slate-500 dark:text-slate-400">Cumulative placements</p>
|
|
<div className="h-56 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={placementData} margin={{ top: 4, right: 8, left: -16, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id="placementStroke" x1="0" y1="0" x2="1" y2="0">
|
|
<stop offset="0%" stopColor={colors.primary[500]} />
|
|
<stop offset="100%" stopColor={colors.accent[500]} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke={colors.neutral[200]} />
|
|
<XAxis dataKey="month" tick={axisTick} stroke={colors.neutral[300]} />
|
|
<YAxis tick={axisTick} stroke={colors.neutral[300]} />
|
|
<Tooltip contentStyle={tooltipStyle} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="placements"
|
|
stroke="url(#placementStroke)"
|
|
strokeWidth={3}
|
|
dot={{ r: 4, fill: colors.primary[600] }}
|
|
activeDot={{ r: 6 }}
|
|
name="Cumulative placements"
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |