147 lines
6.3 KiB
TypeScript
147 lines
6.3 KiB
TypeScript
import { useState } from "react";
|
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { client } from "@/client";
|
|
import { GitCommitHorizontal, Search } from "lucide-react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { cn } from "@/lib/utils";
|
|
import { workspaceColor, workspaceInitial } from "@/components/shell/shared";
|
|
import PaginationBar from "./pagination";
|
|
|
|
function formatTimeAgo(timeSecs: number) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = now - timeSecs;
|
|
if (diff < 60) return "just now";
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`;
|
|
return new Date(timeSecs * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
}
|
|
|
|
function AuthorAvatar({ author }: { author: { name: string } }) {
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"grid size-5 place-items-center overflow-hidden rounded-full bg-gradient-to-br text-[10px] font-bold text-white",
|
|
workspaceColor(author.name),
|
|
)}
|
|
>
|
|
{workspaceInitial(author.name)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const LIMIT = 30;
|
|
|
|
export default function CommitsTab() {
|
|
const { projectName = "", repoName = "" } = useParams();
|
|
const [commitsSearchParams, setCommitsSearchParams] = useSearchParams();
|
|
const offset = Number(commitsSearchParams.get("offset") ?? "0");
|
|
const setOffset = (n: number) => {
|
|
const next = new URLSearchParams(commitsSearchParams);
|
|
if (n <= 0) next.delete("offset"); else next.set("offset", String(n));
|
|
setCommitsSearchParams(next, { replace: true });
|
|
};
|
|
|
|
const { data: repoData } = useQuery({
|
|
queryKey: ["repo", projectName, repoName],
|
|
queryFn: async () => {
|
|
const res = await client.gitGetRepo(projectName, repoName);
|
|
return res.data;
|
|
},
|
|
enabled: Boolean(projectName) && Boolean(repoName),
|
|
retry: false,
|
|
});
|
|
|
|
const defaultBranch = repoData?.default_branch ?? "main";
|
|
const [search, setSearch] = useState("");
|
|
const [selectedBranch, setSelectedBranch] = useState(defaultBranch);
|
|
|
|
const { data: commits = [], isLoading } = useQuery({
|
|
queryKey: ["repo", projectName, repoName, "commits", selectedBranch, offset],
|
|
queryFn: async () => {
|
|
const res = await client.gitCommitHistory(projectName, repoName, { branch: selectedBranch, skip: offset, limit: LIMIT });
|
|
return res.data.commits;
|
|
},
|
|
enabled: Boolean(projectName) && Boolean(repoName),
|
|
retry: false,
|
|
});
|
|
|
|
const { data: branches = [] } = useQuery({
|
|
queryKey: ["repo", projectName, repoName, "branches"],
|
|
queryFn: async () => {
|
|
const res = await client.gitListBranches(projectName, repoName);
|
|
return res.data.branches;
|
|
},
|
|
enabled: Boolean(projectName) && Boolean(repoName),
|
|
retry: false,
|
|
});
|
|
|
|
const filtered = search
|
|
? commits.filter((c) => c.message.toLowerCase().includes(search.toLowerCase()) || c.oid.startsWith(search))
|
|
: commits;
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<select
|
|
className="h-8 rounded-sm border border-border bg-card px-2 text-[12px] font-mono text-foreground"
|
|
value={selectedBranch}
|
|
onChange={(e) => { setSelectedBranch(e.target.value); setOffset(0); }}
|
|
>
|
|
{branches.map((b) => (
|
|
<option key={b.name} value={b.name}>{b.name}</option>
|
|
))}
|
|
</select>
|
|
<div className="ml-auto relative">
|
|
<Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
<Input className="h-7 w-52 rounded-sm border-border bg-card pl-7 text-[12px]" placeholder="Search commits..." value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-md border border-border overflow-hidden">
|
|
{isLoading ? (
|
|
<div className="px-4 py-2">
|
|
{Array.from({ length: 8 }).map((_, i) => <div className="h-6 animate-pulse rounded bg-muted/50 my-1.5" key={i} />)}
|
|
</div>
|
|
) : filtered.length > 0 ? (
|
|
<>
|
|
<div>
|
|
{filtered.map((commit) => (
|
|
<Link
|
|
className="flex items-center gap-3 border-b border-border px-4 py-2.5 last:border-b-0 transition-colors hover:bg-muted/40"
|
|
key={commit.oid}
|
|
to={`/${projectName}/repo/${repoName}/commits/${commit.oid}`}
|
|
>
|
|
<GitCommitHorizontal className="size-4 text-muted-foreground shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<span className="font-heading text-[13px] font-semibold text-foreground truncate block leading-tight">{commit.summary}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<AuthorAvatar author={{ name: commit.author?.name ?? "unknown" }} />
|
|
<span className="font-mono text-[11px] text-muted-foreground">{commit.author?.name}</span>
|
|
<span className="font-mono text-[11px] text-muted-foreground">{commit.author?.time_secs ? formatTimeAgo(commit.author.time_secs) : ""}</span>
|
|
<code className="rounded bg-muted/60 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">{commit.oid.slice(0, 7)}</code>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
<PaginationBar offset={offset} limit={LIMIT} hasMore={commits.length >= LIMIT} onPrev={() => setOffset(Math.max(0, offset - LIMIT))} onNext={() => setOffset(offset + LIMIT)} />
|
|
</>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="grid size-14 place-items-center rounded-2xl bg-muted/40">
|
|
<GitCommitHorizontal className="size-6 text-muted-foreground/40" />
|
|
</div>
|
|
<h3 className="mt-4 font-heading text-sm font-semibold text-foreground">
|
|
{search ? "No matching commits" : "No commits yet"}
|
|
</h3>
|
|
<p className="mt-1.5 max-w-[280px] text-[12px] leading-relaxed text-muted-foreground">
|
|
{search ? `No commits match "${search}"` : "Push your first commit to this branch to get started."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |