CommitList.tsx

  1import { useState, useEffect } from 'react'
  2import { Link } from 'react-router-dom'
  3import { formatDistanceToNow } from 'date-fns'
  4import { GitCommit } from 'lucide-react'
  5import { Button } from '@/components/ui/button'
  6import { Skeleton } from '@/components/ui/skeleton'
  7import { getCommits } from '@/lib/gitApi'
  8import type { GitCommit as GitCommitType } from '@/lib/gitApi'
  9
 10interface CommitListProps {
 11  ref_: string
 12  path?: string
 13}
 14
 15const PAGE_SIZE = 30
 16
 17// Paginated commit history grouped by calendar date. Each row links to the
 18// commit detail page. Used in CodePage's "History" view.
 19export function CommitList({ ref_, path }: CommitListProps) {
 20  const [commits, setCommits] = useState<GitCommitType[]>([])
 21  const [loading, setLoading] = useState(true)
 22  const [loadingMore, setLoadingMore] = useState(false)
 23  const [error, setError] = useState<string | null>(null)
 24  const [hasMore, setHasMore] = useState(true)
 25
 26  useEffect(() => {
 27    setCommits([])
 28    setHasMore(true)
 29    setError(null)
 30    setLoading(true)
 31    getCommits(ref_, { path, limit: PAGE_SIZE })
 32      .then((data) => {
 33        setCommits(data)
 34        setHasMore(data.length === PAGE_SIZE)
 35      })
 36      .catch((e: Error) => setError(e.message))
 37      .finally(() => setLoading(false))
 38  }, [ref_, path])
 39
 40  function loadMore() {
 41    const last = commits[commits.length - 1]
 42    if (!last) return
 43    setLoadingMore(true)
 44    getCommits(ref_, { path, limit: PAGE_SIZE, after: last.hash })
 45      .then((data) => {
 46        setCommits((prev) => [...prev, ...data])
 47        setHasMore(data.length === PAGE_SIZE)
 48      })
 49      .catch((e: Error) => setError(e.message))
 50      .finally(() => setLoadingMore(false))
 51  }
 52
 53  if (loading) return <CommitListSkeleton />
 54
 55  if (error) {
 56    return (
 57      <div className="rounded-md border border-border px-4 py-8 text-center text-sm text-destructive">
 58        {error}
 59      </div>
 60    )
 61  }
 62
 63  // Group commits by date (YYYY-MM-DD)
 64  const groups = groupByDate(commits)
 65
 66  return (
 67    <div className="space-y-6">
 68      {groups.map(([date, group]) => (
 69        <div key={date}>
 70          <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
 71            Commits on {date}
 72          </h3>
 73          <div className="overflow-hidden rounded-md border border-border divide-y divide-border">
 74            {group.map((commit) => (
 75              <CommitRow key={commit.hash} commit={commit} />
 76            ))}
 77          </div>
 78        </div>
 79      ))}
 80
 81      {hasMore && (
 82        <div className="text-center">
 83          <Button variant="outline" size="sm" onClick={loadMore} disabled={loadingMore}>
 84            {loadingMore ? 'Loading…' : 'Load more commits'}
 85          </Button>
 86        </div>
 87      )}
 88    </div>
 89  )
 90}
 91
 92function CommitRow({ commit }: { commit: GitCommitType }) {
 93  return (
 94    <div className="flex items-center gap-3 bg-background px-4 py-3 hover:bg-muted/30">
 95      <GitCommit className="size-4 shrink-0 text-muted-foreground" />
 96
 97      <div className="min-w-0 flex-1">
 98        <Link
 99          to={`/commit/${commit.hash}`}
100          className="block truncate font-medium text-foreground hover:text-primary hover:underline"
101        >
102          {commit.message}
103        </Link>
104        <p className="mt-0.5 text-xs text-muted-foreground">
105          {commit.authorName} &middot;{' '}
106          {formatDistanceToNow(new Date(commit.date), { addSuffix: true })}
107        </p>
108      </div>
109
110      <Link
111        to={`/commit/${commit.hash}`}
112        className="shrink-0 font-mono text-xs text-muted-foreground hover:text-foreground hover:underline"
113        title={commit.hash}
114      >
115        {commit.shortHash}
116      </Link>
117    </div>
118  )
119}
120
121function groupByDate(commits: GitCommitType[]): [string, GitCommitType[]][] {
122  const map = new Map<string, GitCommitType[]>()
123  for (const c of commits) {
124    const date = new Date(c.date).toLocaleDateString('en-US', {
125      year: 'numeric',
126      month: 'long',
127      day: 'numeric',
128    })
129    const group = map.get(date) ?? []
130    group.push(c)
131    map.set(date, group)
132  }
133  return Array.from(map.entries())
134}
135
136function CommitListSkeleton() {
137  return (
138    <div className="space-y-6">
139      {Array.from({ length: 2 }).map((_, g) => (
140        <div key={g}>
141          <Skeleton className="mb-2 h-3 w-32" />
142          <div className="overflow-hidden rounded-md border border-border divide-y divide-border">
143            {Array.from({ length: 4 }).map((_, i) => (
144              <div key={i} className="flex items-center gap-3 px-4 py-3">
145                <Skeleton className="size-4 rounded" />
146                <div className="flex-1 space-y-1.5">
147                  <Skeleton className="h-4 w-2/3" />
148                  <Skeleton className="h-3 w-1/4" />
149                </div>
150                <Skeleton className="h-3 w-14" />
151              </div>
152            ))}
153          </div>
154        </div>
155      ))}
156    </div>
157  )
158}