"use client"; import { useEffect, useReducer } from "react"; import { IconGitPullRequest, IconEye, IconMessage, IconCheck, IconClock, } from "@tabler/icons-react"; import { Card, CardContent } from "@kandev/ui/card"; import { Spinner } from "@kandev/ui/spinner"; import { fetchGitHubStats } from "@/lib/api/domains/github-api"; import type { PRStats } from "@/lib/types/github"; import { useTranslation } from "react-i18next"; type StatCardProps = { icon: React.ReactNode; label: string; value: string | number; subtext?: string; }; function StatCard({ icon, label, value, subtext }: StatCardProps) { return (
{icon}

{value}

{label}

{subtext &&

{subtext}

}
); } function formatHours(hours: number): string { if (hours < 1) return "< 1h"; if (hours < 24) return `${Math.round(hours)}h`; const days = Math.round(hours / 24); return `${days}d`; } function formatPercent(rate: number): string { return `${Math.round(rate * 100)}%`; } type StatsState = { stats: PRStats | null; loading: boolean }; type StatsAction = { type: "fetch" } | { type: "done"; stats: PRStats | null }; function statsReducer(state: StatsState, action: StatsAction): StatsState { switch (action.type) { case "fetch": return { ...state, loading: true }; case "done": return { stats: action.stats, loading: false }; } } function usePRStats(workspaceId: string | null) { const [state, dispatch] = useReducer(statsReducer, { stats: null, loading: true }); useEffect(() => { let cancelled = false; dispatch({ type: "fetch" }); fetchGitHubStats(workspaceId ? { workspace_id: workspaceId } : {}, { cache: "no-store" }) .then((data) => { if (!cancelled) dispatch({ type: "done", stats: data }); }) .catch(() => { if (!cancelled) dispatch({ type: "done", stats: null }); }); return () => { cancelled = true; }; }, [workspaceId]); return state; } function StatsGrid({ stats }: { stats: PRStats }) { const { t } = useTranslation(); return (
} label={t("github:prsCreated")} value={stats.total_prs_created} /> } label={t("github:prsReviewed")} value={stats.total_prs_reviewed} /> } label={t("github:comments")} value={stats.total_comments} /> } label={t("github:ciPassRate")} value={formatPercent(stats.ci_pass_rate)} /> } label={t("github:approvalRate")} value={formatPercent(stats.approval_rate)} /> } label={t("github:avgTimeToMerge")} value={formatHours(stats.avg_time_to_merge_hours)} />
); } function PRsByDayChart({ prsByDay }: { prsByDay: PRStats["prs_by_day"] }) { const { t } = useTranslation(); if (!prsByDay || prsByDay.length === 0) return null; const maxCount = Math.max(...prsByDay.map((d) => d.count), 1); return (

{t("github:prsByDay")}

{prsByDay.map((day) => { const height = Math.max((day.count / maxCount) * 100, 4); return (
); })}
{prsByDay[0]?.date} {prsByDay[prsByDay.length - 1]?.date}
); } export function PRStatsPanel({ workspaceId }: { workspaceId: string | null }) { const { t } = useTranslation(); const { stats, loading } = usePRStats(workspaceId); if (loading) { return (
); } if (!stats) { return (

{t("github:noStatsAvailableYet")}

); } return (
); }