cpns / apps /web /src /components /analytics /BreakdownCharts.tsx
pramudya-yb
Rename project from Labas to Pram
2b62b0c
Raw
History Blame Contribute Delete
8.45 kB
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@pram/ui/components/card";
import { formatLabel } from "@/lib/format";
import { formatTime } from "@/lib/time";
export interface ExamTypeItem {
examTypeId: string;
examTypeName: string;
accuracyPct: number;
avgScorePct: number;
avgTimeSpentSec: number;
attempts: number;
}
export interface SectionTypeItem {
sectionTypeId: string;
sectionTypeName: string;
accuracyPct: number;
avgScorePct: number;
avgTimeSpentSec: number;
attempts: number;
}
export interface FormatItem {
format: string;
accuracyPct: number;
avgTimeSpentSec: number;
totalQuestions: number;
}
interface BreakdownChartsProps {
byExamType: ExamTypeItem[] | undefined;
bySectionType: SectionTypeItem[] | undefined;
byFormat: FormatItem[] | undefined;
}
const COLORS = [
"var(--matcha-600)",
"var(--slushie-600)",
"var(--lemon-700)",
"var(--ube-600)",
"var(--pomegranate-600)",
];
function AccuracyBar({ pct }: { pct: number }) {
return (
<div className="w-full h-2 bg-[var(--oat-light)] rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${pct}%`,
backgroundColor:
pct >= 80
? "var(--matcha-600)"
: pct >= 60
? "var(--lemon-700)"
: "var(--pomegranate-600)",
}}
/>
</div>
);
}
export function BreakdownCharts({ byExamType, bySectionType, byFormat }: BreakdownChartsProps) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="clay-shadow bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[var(--radius-xl)]">
<CardHeader>
<CardTitle className="text-lg font-headline font-bold text-[var(--clay-black)]">
Performa per Jenis Ujian
</CardTitle>
</CardHeader>
<CardContent>
{byExamType && byExamType.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<BarChart data={byExamType} margin={{ top: 5, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--oat-border)" />
<XAxis
dataKey="examTypeName"
tick={{ fontSize: 12, fill: "var(--warm-charcoal)" }}
axisLine={{ stroke: "var(--oat-border)" }}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 12, fill: "var(--warm-charcoal)" }}
axisLine={false}
tickLine={false}
domain={[0, 100]}
unit="%"
/>
<Tooltip
contentStyle={{
backgroundColor: "var(--pure-white)",
border: "2px solid var(--oat-border)",
borderRadius: "var(--radius-lg)",
fontSize: 13,
}}
formatter={(value: unknown, name: unknown, _item: unknown, _index: unknown, _payload: unknown) => {
const num = typeof value === "number" ? value : 0;
const n = typeof name === "string" ? name : "";
if (n === "accuracyPct") return [`${num}%`, "Akurasi"];
if (n === "avgScorePct") return [`${num}%`, "Rata-rata Skor"];
return [num, n];
}}
/>
<Bar dataKey="accuracyPct" radius={[6, 6, 0, 0]}>
{byExamType.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-56 flex items-center justify-center text-[var(--warm-charcoal)]">
Belum ada data.
</div>
)}
</CardContent>
</Card>
<Card className="clay-shadow bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[var(--radius-xl)]">
<CardHeader>
<CardTitle className="text-lg font-headline font-bold text-[var(--clay-black)]">
Performa per Section
</CardTitle>
</CardHeader>
<CardContent>
{bySectionType && bySectionType.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<BarChart data={bySectionType} margin={{ top: 5, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--oat-border)" />
<XAxis
dataKey="sectionTypeName"
tick={{ fontSize: 12, fill: "var(--warm-charcoal)" }}
axisLine={{ stroke: "var(--oat-border)" }}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 12, fill: "var(--warm-charcoal)" }}
axisLine={false}
tickLine={false}
domain={[0, 100]}
unit="%"
/>
<Tooltip
contentStyle={{
backgroundColor: "var(--pure-white)",
border: "2px solid var(--oat-border)",
borderRadius: "var(--radius-lg)",
fontSize: 13,
}}
formatter={(value: unknown, name: unknown, _item: unknown, _index: unknown, _payload: unknown) => {
const num = typeof value === "number" ? value : 0;
const n = typeof name === "string" ? name : "";
if (n === "accuracyPct") return [`${num}%`, "Akurasi"];
if (n === "avgScorePct") return [`${num}%`, "Rata-rata Skor"];
return [num, n];
}}
/>
<Bar dataKey="accuracyPct" radius={[6, 6, 0, 0]}>
{bySectionType.map((_, i) => (
<Cell key={i} fill={COLORS[(i + 2) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-56 flex items-center justify-center text-[var(--warm-charcoal)]">
Belum ada data.
</div>
)}
</CardContent>
</Card>
<Card className="clay-shadow bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[var(--radius-xl)] lg:col-span-2">
<CardHeader>
<CardTitle className="text-lg font-headline font-bold text-[var(--clay-black)]">
Performa per Format Soal
</CardTitle>
</CardHeader>
<CardContent>
{byFormat && byFormat.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{byFormat.map((item) => (
<div
key={item.format}
className="p-4 rounded-[var(--radius-lg)] border-2 border-[var(--oat-border)] bg-[var(--warm-cream)]"
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-[var(--clay-black)]">
{formatLabel(item.format)}
</span>
<span className="text-xs text-[var(--warm-charcoal)]">
{item.totalQuestions} soal
</span>
</div>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg font-bold text-[var(--clay-black)]">
{item.accuracyPct}%
</span>
<span className="text-xs text-[var(--warm-charcoal)]">
{formatTime(item.avgTimeSpentSec)}/soal
</span>
</div>
<AccuracyBar pct={item.accuracyPct} />
</div>
))}
</div>
) : (
<div className="h-20 flex items-center justify-center text-[var(--warm-charcoal)]">
Belum ada data.
</div>
)}
</CardContent>
</Card>
</div>
);
}