11import { useState , useRef , useMemo } from 'react'
22import { motion } from 'framer-motion'
3- import { FolderGit2 , Plus } from 'lucide-react'
3+ import { FolderGit2 , Plus , Files , FunctionSquare , Clock } from 'lucide-react'
4+ import { cn } from '@/lib/utils'
45import type { Repository } from '../types'
56import { RepoGridSkeleton } from './ui/Skeleton'
67
@@ -12,38 +13,71 @@ interface RepoListProps {
1213 loading ?: boolean
1314}
1415
15- const StatusBadge = ( { status } : { status : string } ) => {
16+ type SortMode = 'recent' | 'name' | 'size'
17+
18+ /** Extract "owner/repo" from a GitHub URL */
19+ function parseRepoSlug ( gitUrl : string ) : string {
20+ try {
21+ const cleaned = gitUrl . replace ( / \. g i t $ / , '' )
22+ const match = cleaned . match ( / g i t h u b \. c o m \/ ( [ ^ / ] + \/ [ ^ / ] + ) / )
23+ return match ? match [ 1 ] : ''
24+ } catch {
25+ return ''
26+ }
27+ }
28+
29+ /** Relative time: "2h ago", "3d ago", "just now" */
30+ function timeAgo ( dateStr ?: string ) : string {
31+ if ( ! dateStr ) return ''
32+ const now = Date . now ( )
33+ const then = new Date ( dateStr ) . getTime ( )
34+ const diff = now - then
35+ if ( diff < 0 ) return ''
36+
37+ const mins = Math . floor ( diff / 60_000 )
38+ if ( mins < 1 ) return 'just now'
39+ if ( mins < 60 ) return `${ mins } m ago`
40+ const hrs = Math . floor ( mins / 60 )
41+ if ( hrs < 24 ) return `${ hrs } h ago`
42+ const days = Math . floor ( hrs / 24 )
43+ if ( days < 30 ) return `${ days } d ago`
44+ return new Date ( dateStr ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' } )
45+ }
46+
47+ const StatusDot = ( { status } : { status : string } ) => {
1648 const isIndexed = status === 'indexed'
17-
1849 return (
19- < span className = { `inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full border
20- ${ isIndexed
21- ? 'bg-primary/10 text-primary border-primary/20'
22- : 'bg-muted text-muted-foreground border-border'
23- } `}
24- >
25- < span className = { `w-1.5 h-1.5 rounded-full ${ isIndexed ? 'bg-primary' : 'bg-muted-foreground animate-pulse' } ` } />
50+ < span className = { cn (
51+ 'inline-flex items-center gap-1.5 text-xs' ,
52+ isIndexed ? 'text-primary' : 'text-muted-foreground' ,
53+ ) } >
54+ < span className = { cn (
55+ 'w-1.5 h-1.5 rounded-full' ,
56+ isIndexed ? 'bg-primary' : 'bg-muted-foreground animate-pulse' ,
57+ ) } />
2658 { isIndexed ? 'Indexed' : 'Pending' }
2759 </ span >
2860 )
2961}
3062
31- const RepoCard = ( { repo, index, onSelect } : {
63+ const RepoCard = ( { repo, index, onSelect } : {
3264 repo : Repository
3365 index : number
34- onSelect : ( ) => void
66+ onSelect : ( ) => void
3567} ) => {
3668 const cardRef = useRef < HTMLButtonElement > ( null )
3769 const [ mousePos , setMousePos ] = useState ( { x : 0 , y : 0 } )
3870 const [ hovering , setHovering ] = useState ( false )
71+ const slug = parseRepoSlug ( repo . git_url )
72+ const indexed = timeAgo ( repo . last_indexed_at )
3973
4074 return (
4175 < motion . button
4276 ref = { cardRef }
43- initial = { { opacity : 0 , y : 20 } }
77+ initial = { { opacity : 0 , y : 12 } }
4478 animate = { { opacity : 1 , y : 0 } }
45- transition = { { delay : index * 0.05 , duration : 0.3 } }
46- whileHover = { { y : - 3 } }
79+ transition = { { delay : index * 0.04 , duration : 0.25 } }
80+ whileHover = { { y : - 2 } }
4781 onClick = { onSelect }
4882 onMouseMove = { ( e ) => {
4983 if ( ! cardRef . current ) return
@@ -56,7 +90,6 @@ const RepoCard = ({ repo, index, onSelect }: {
5690 bg-card border border-border hover:border-primary/40
5791 focus:outline-none focus:ring-2 focus:ring-primary/50 p-5 transition-colors"
5892 >
59- { /* Mouse glow effect */ }
6093 { hovering && (
6194 < div
6295 className = "pointer-events-none absolute inset-0"
@@ -65,45 +98,84 @@ const RepoCard = ({ repo, index, onSelect }: {
6598 } }
6699 />
67100 ) }
68-
101+
69102 < div className = "relative" >
70- { /* Header */ }
71- < div className = "flex items-start justify-between mb-4 " >
72- < div className = "w-11 h-11 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center group-hover:bg-primary/15 transition-colors" >
103+ { /* Top row: icon + status */ }
104+ < div className = "flex items-start justify-between mb-3 " >
105+ < div className = "w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center group-hover:bg-primary/15 transition-colors" >
73106 < FolderGit2 className = "w-5 h-5 text-primary" />
74107 </ div >
75- < StatusBadge status = { repo . status } />
108+ < StatusDot status = { repo . status } />
76109 </ div >
77110
78- { /* Title */ }
79- < h3 className = "text-lg font-semibold text-foreground mb-0.5 group-hover:text-primary transition-colors" >
111+ { /* Repo name + slug */ }
112+ < h3 className = "text-base font-semibold text-foreground group-hover:text-primary transition-colors truncate " >
80113 { repo . name }
81114 </ h3 >
82- < p className = "text-xs text-muted-foreground font-mono mb-5" > { repo . branch } </ p >
83-
84- { /* Stats */ }
85- < div className = "pt-4 border-t border-border" >
86- < div className = "flex items-center justify-between" >
87- < span className = "text-sm text-muted-foreground" > Files</ span >
88- < span className = "text-2xl font-bold text-primary" >
89- { ( repo . file_count || 0 ) . toLocaleString ( ) }
115+ { slug && (
116+ < p className = "text-xs text-muted-foreground truncate mt-0.5" > { slug } </ p >
117+ ) }
118+
119+ { /* Stats row */ }
120+ < div className = "flex items-center gap-3 mt-4 pt-3 border-t border-border text-xs text-muted-foreground" >
121+ < span className = "flex items-center gap-1" >
122+ < Files className = "w-3 h-3" />
123+ { ( repo . file_count || 0 ) . toLocaleString ( ) }
124+ </ span >
125+ { repo . function_count != null && repo . function_count > 0 && (
126+ < span className = "flex items-center gap-1" >
127+ < FunctionSquare className = "w-3 h-3" />
128+ { repo . function_count . toLocaleString ( ) }
90129 </ span >
91- </ div >
130+ ) }
131+ { indexed && (
132+ < span className = "flex items-center gap-1 ml-auto" >
133+ < Clock className = "w-3 h-3" />
134+ { indexed }
135+ </ span >
136+ ) }
92137 </ div >
93138 </ div >
94139 </ motion . button >
95140 )
96141}
97142
143+ const SortTab = ( { label, active, onClick } : {
144+ label : string
145+ active : boolean
146+ onClick : ( ) => void
147+ } ) => (
148+ < button
149+ onClick = { onClick }
150+ className = { cn (
151+ 'text-xs px-3 py-1 rounded-full transition-colors' ,
152+ active
153+ ? 'bg-primary/10 text-primary font-medium'
154+ : 'text-muted-foreground hover:text-foreground' ,
155+ ) }
156+ >
157+ { label }
158+ </ button >
159+ )
160+
98161export function RepoList ( { repos, selectedRepo, onSelect, onAddClick, loading } : RepoListProps ) {
99- // Hooks must be called before any conditional returns
162+ const [ sortMode , setSortMode ] = useState < SortMode > ( 'recent' )
163+
100164 const sortedRepos = useMemo ( ( ) => {
101- return [ ...repos ] . sort ( ( a , b ) => {
102- if ( a . status === 'indexed' && b . status !== 'indexed' ) return - 1
103- if ( b . status === 'indexed' && a . status !== 'indexed' ) return 1
104- return ( b . file_count || 0 ) - ( a . file_count || 0 )
105- } )
106- } , [ repos ] )
165+ const sorted = [ ...repos ]
166+ if ( sortMode === 'recent' ) {
167+ sorted . sort ( ( a , b ) => {
168+ const aTime = a . last_indexed_at || a . created_at || ''
169+ const bTime = b . last_indexed_at || b . created_at || ''
170+ return bTime . localeCompare ( aTime )
171+ } )
172+ } else if ( sortMode === 'name' ) {
173+ sorted . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
174+ } else {
175+ sorted . sort ( ( a , b ) => ( b . file_count || 0 ) - ( a . file_count || 0 ) )
176+ }
177+ return sorted
178+ } , [ repos , sortMode ] )
107179
108180 if ( loading ) return < RepoGridSkeleton count = { 3 } />
109181
@@ -133,15 +205,26 @@ export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }:
133205 }
134206
135207 return (
136- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" >
137- { sortedRepos . map ( ( repo , index ) => (
138- < RepoCard
139- key = { repo . id }
140- repo = { repo }
141- index = { index }
142- onSelect = { ( ) => onSelect ( repo . id ) }
143- />
144- ) ) }
208+ < div className = "space-y-4" >
209+ { /* Sort bar */ }
210+ < div className = "flex items-center gap-1" >
211+ < SortTab label = "Recent" active = { sortMode === 'recent' } onClick = { ( ) => setSortMode ( 'recent' ) } />
212+ < SortTab label = "Name" active = { sortMode === 'name' } onClick = { ( ) => setSortMode ( 'name' ) } />
213+ < SortTab label = "Size" active = { sortMode === 'size' } onClick = { ( ) => setSortMode ( 'size' ) } />
214+ < span className = "ml-auto text-xs text-muted-foreground" > { repos . length } repos</ span >
215+ </ div >
216+
217+ { /* Grid */ }
218+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" >
219+ { sortedRepos . map ( ( repo , index ) => (
220+ < RepoCard
221+ key = { repo . id }
222+ repo = { repo }
223+ index = { index }
224+ onSelect = { ( ) => onSelect ( repo . id ) }
225+ />
226+ ) ) }
227+ </ div >
145228 </ div >
146229 )
147230}
0 commit comments