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} ·{' '}
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}