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