import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { format, formatDistanceToNow } from "date-fns";
import { CheckIcon, CopyIcon, GitCommitIcon } from "lucide-react";
import { type MouseEventHandler, useState } from "react";
import * as z from "zod";
import { getCommitsQueryOptions } from "@/api/commits";
import { NotFoundComponent } from "@/components/404-components";
import { BranchSelector } from "@/components/branch-selector";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
const searchSchema = z.object({
ref: z.string().optional(),
});
export const Route = createFileRoute("/$owner/$repo/_layout/commits")({
component: RouteComponent,
notFoundComponent: NotFoundComponent,
validateSearch: searchSchema,
loaderDeps: ({ search }) => ({
ref: search.ref,
}),
loader: async ({ params, context: { queryClient }, deps }) => {
await queryClient.ensureQueryData(
getCommitsQueryOptions({
owner: params.owner,
repo: params.repo,
ref: deps.ref,
})
);
},
pendingComponent: CommitsPendingComponent,
});
function CommitsPendingComponent() {
return (
<div>
{/* Header with title and branch selector skeleton */}
<div className="mb-6 flex items-center justify-between">
<h1 className="font-semibold text-2xl">Commits</h1>
<Skeleton className="h-9 w-[180px]" />
</div>
<div className="space-y-8">
{/* Render 2 date groups */}
{[1, 2].map((groupIndex) => (
<div key={groupIndex}>
{/* Date Header Skeleton */}
<div className="mb-3">
<Skeleton className="h-5 w-32" />
</div>
{/* Commits List Skeleton */}
<div className="divide-y overflow-hidden rounded-lg border">
{[1, 2, 3].map((commitIndex) => (
<div className="flex items-start gap-3 p-4" key={commitIndex}>
{/* Commit Icon Skeleton */}
<div className="mt-1 shrink-0">
<Skeleton className="size-4 rounded-full" />
</div>
{/* Main Content Skeleton */}
<div className="min-w-0 flex-1 space-y-2">
{/* Commit Message Skeleton */}
<Skeleton className="h-5 w-3/4" />
{/* Author and Time Skeleton */}
<div className="flex items-center gap-1">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-24" />
</div>
</div>
{/* Commit Hash and Actions Skeleton */}
<div className="flex shrink-0 items-center gap-2">
<Skeleton className="h-4 w-14" />
<Skeleton className="size-9" />
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
function formatRelativeTime(timestamp: number): string {
const date = new Date(timestamp * 1000);
return formatDistanceToNow(date, { addSuffix: true });
}
function formatCommitDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
const now = new Date();
const isThisYear = date.getFullYear() === now.getFullYear();
return format(date, isThisYear ? "MMM d" : "MMM d, yyyy");
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy: MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
e.stopPropagation();
copyToClipboard(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
return (
<Button onClick={handleCopy} size="icon" variant="outline">
{copied ? (
<CheckIcon className="size-4 text-green-600" />
) : (
<CopyIcon className="size-4" />
)}
</Button>
);
}
function RouteComponent() {
const params = Route.useParams();
const { owner, repo } = params;
const { ref } = Route.useSearch();
const navigate = useNavigate();
const { data } = useSuspenseQuery(
getCommitsQueryOptions({
owner,
repo,
ref,
})
);
type CommitType = (typeof data)[number];
// Group commits by date
const groupedCommits = data.reduce(
(acc, commit) => {
const dateKey = formatCommitDate(commit.commit.author.timestamp);
if (!acc[dateKey]) {
acc[dateKey] = [];
}
acc[dateKey].push(commit);
return acc;
},
{} as Record<string, CommitType[]>
);
return (
<div className="py-6">
{/* Branch Selector */}
<div className="mb-6 flex items-center justify-between">
<h1 className="font-semibold text-2xl">Commits</h1>
<BranchSelector
onBranchChange={(newBranch) => {
navigate({
to: ".",
search: { ref: newBranch },
});
}}
owner={owner}
repo={repo}
selectedBranch={ref}
/>
</div>
<div className="space-y-8">
{Object.entries(groupedCommits).map(([date, commits]) => (
<div key={date}>
{/* Date Header */}
<div className="mb-3 flex items-center gap-2">
<h2 className="font-semibold text-foreground text-sm">
Commits on {date}
</h2>
</div>
{/* Commits List */}
<div className="divide-y overflow-hidden rounded-lg border">
{commits.map((commit, index) => {
const shortHash = commit.oid.substring(0, 7);
const commitMessage = commit.commit.message.split("\n")[0];
return (
<Link
className="block"
key={commit.oid}
params={{
owner,
repo,
commitId: commit.oid,
}}
to="/$owner/$repo/commits/$commitId"
>
<div
className={cn(
"group relative flex items-start gap-3 p-4 transition-colors hover:bg-muted/50",
index === 0 && "rounded-t-lg",
index === commits.length - 1 && "rounded-b-lg"
)}
>
{/* Commit Icon */}
<div className="mt-1 shrink-0">
<GitCommitIcon className="size-4 text-muted-foreground" />
</div>
{/* Main Content */}
<div className="min-w-0 flex-1">
{/* Commit Message */}
<div className="mb-1">
<span className="font-semibold text-foreground">
{commitMessage}
</span>
</div>
{/* Author and Time */}
<div className="flex flex-wrap items-center gap-1 text-muted-foreground text-xs">
<span className="font-medium">
{commit.commit.author.name}
</span>
<span>committed</span>
<span>
{formatRelativeTime(commit.commit.author.timestamp)}
</span>
</div>
</div>
{/* Commit Hash and Actions */}
<div className="flex shrink-0 items-center gap-2">
<span className="text-xs">{shortHash}</span>
<CopyButton text={commit.oid} />
</div>
</div>
</Link>
);
})}
</div>
</div>
))}
{data.length === 0 && (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
<GitCommitIcon className="mb-4 size-12 text-muted-foreground" />
<h3 className="mb-2 font-semibold text-lg">No commits yet</h3>
<p className="text-muted-foreground text-sm">
This repository doesn't have any commits yet.
</p>
</div>
)}
</div>
</div>
);
}