gitdataai/src/app/repository/contributors.tsx
2026-04-15 09:08:09 +08:00

110 lines
3.6 KiB
TypeScript

import { gitContributors } from "@/client";
import type { ContributorStats } from "@/client";
import { RepoHeader } from "@/components/repository/header";
import { useQuery } from "@tanstack/react-query";
import { Hash, Loader2, Mail, Calendar } from "lucide-react";
import { useParams } from "react-router-dom";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
function formatDate(ts: number | null | undefined): string {
if (!ts) return "—";
return new Date(ts * 1000).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
function ContributorCard({ c }: { c: ContributorStats }) {
const initials = c.name
? c.name
.split(/\s+/)
.slice(0, 2)
.map((p) => p[0])
.join("")
.toUpperCase()
: c.email[0].toUpperCase();
return (
<div className="flex items-start gap-3 border rounded-lg px-4 py-3 hover:bg-muted/50 transition-colors">
<Avatar className="h-9 w-9 flex-shrink-0">
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{c.name || "—"}</p>
<div className="flex items-center gap-1.5 mt-0.5">
<Mail className="h-3 w-3 text-muted-foreground flex-shrink-0" />
<span className="text-xs text-muted-foreground truncate">{c.email}</span>
</div>
</div>
<div className="flex flex-col items-end gap-1 flex-shrink-0">
<span className="text-sm font-semibold tabular-nums">{c.commits.toLocaleString()}</span>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
{formatDate(c.last_commit_at)}
</div>
</div>
</div>
);
}
export const RepoContributors = () => {
const { namespace, repoName } = useParams<{ namespace: string; repoName: string }>();
const { data, isLoading } = useQuery({
queryKey: ["repo-contributors", namespace, repoName],
queryFn: async () => {
if (!namespace || !repoName) return null;
const resp = await gitContributors({ path: { namespace, repo: repoName } });
return resp.data?.data ?? null;
},
enabled: !!namespace && !!repoName,
staleTime: 60 * 1000,
});
const contributors: ContributorStats[] = data?.contributors ?? [];
const total: number = data?.total ?? 0;
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
);
}
return (
<>
<div className="p-6 pb-0 max-w-5xl mx-auto space-y-6">
<RepoHeader />
</div>
<div className="p-6 pt-0 max-w-5xl mx-auto space-y-4">
<div className="flex items-center justify-between py-4">
<div>
<h2 className="text-lg font-semibold">Contributors</h2>
<p className="text-sm text-muted-foreground">
{total} contributor{total !== 1 ? "s" : ""} sorted by commit count
</p>
</div>
</div>
{contributors.length === 0 ? (
<div className="p-12 text-center text-muted-foreground border rounded-lg">
<Hash className="h-10 w-10 mx-auto mb-3 opacity-40" />
<p className="font-medium">No contributors found</p>
<p className="text-sm mt-1">This repository has no commits yet.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{contributors.map((c) => (
<ContributorCard key={c.email} c={c} />
))}
</div>
)}
</div>
</>
);
};