"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";
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 }) {
return (
}
label="PRs Created"
value={stats.total_prs_created}
/>
}
label="PRs Reviewed"
value={stats.total_prs_reviewed}
/>
}
label="Comments"
value={stats.total_comments}
/>
}
label="CI Pass Rate"
value={formatPercent(stats.ci_pass_rate)}
/>
}
label="Approval Rate"
value={formatPercent(stats.approval_rate)}
/>
}
label="Avg Time to Merge"
value={formatHours(stats.avg_time_to_merge_hours)}
/>
);
}
function PRsByDayChart({ prsByDay }: { prsByDay: PRStats["prs_by_day"] }) {
if (!prsByDay || prsByDay.length === 0) return null;
const maxCount = Math.max(...prsByDay.map((d) => d.count), 1);
return (
PRs by Day
{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 { stats, loading } = usePRStats(workspaceId);
if (loading) {
return (
);
}
if (!stats) {
return (
No stats available yet.
);
}
return (
);
}