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