RepositoriesCard.tsx
1 import { useRepos } from '@/hooks/useDashboard' 2 import { CiStatusIcon } from '@/components/ui' 3 4 type SyncStatus = 'synced' | 'unsynced' | 'no-radicle' 5 6 const SYNC_COLORS: Record<SyncStatus, string> = { 7 synced: '#3fb950', 8 unsynced: '#d29922', 9 'no-radicle': '#f85149', 10 } 11 12 const SYNC_TITLES: Record<SyncStatus, (repo: { forgejo_head?: string; radicle_head?: string }) => string> = { 13 synced: (r) => `Synced: ${r.forgejo_head ?? '?'} = ${r.radicle_head ?? '?'}`, 14 unsynced: (r) => `Out of sync: Forgejo ${r.forgejo_head ?? '?'} ≠ Radicle ${r.radicle_head ?? '?'}`, 15 'no-radicle': () => 'No Radicle origin', 16 } 17 18 export function RepositoriesCard() { 19 const { data, isLoading, isError } = useRepos() 20 21 const repos = data?.repos ?? data?.repositories ?? [] 22 23 return ( 24 <section 25 className="rounded-lg overflow-hidden flex flex-col" 26 style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)' }} 27 > 28 <h2 29 className="px-6 py-4 text-base font-semibold" 30 style={{ background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border-color)', color: 'var(--text-primary)' }} 31 > 32 Repositories 33 </h2> 34 <div className="p-4"> 35 {isLoading && ( 36 <p className="text-center py-8" style={{ color: 'var(--text-muted)' }}>Loading...</p> 37 )} 38 {isError && ( 39 <p className="text-center py-8" style={{ color: 'var(--text-muted)' }}>Failed to load repositories</p> 40 )} 41 {!isLoading && !isError && repos.length === 0 && ( 42 <p className="text-center py-8" style={{ color: 'var(--text-muted)' }}>No repositories found</p> 43 )} 44 {!isLoading && !isError && repos.length > 0 && ( 45 <ul 46 className="list-none grid gap-2" 47 style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))' }} 48 > 49 {repos.map(repo => { 50 const syncStatus = (repo.sync_status ?? 'no-radicle') as SyncStatus 51 const color = SYNC_COLORS[syncStatus] ?? SYNC_COLORS['no-radicle'] 52 const title = (SYNC_TITLES[syncStatus] ?? SYNC_TITLES['no-radicle'])(repo) 53 const ciStatus = repo.ci_status ?? repo.forgejo_ci_status 54 const unmerged = repo.unmerged_branches ?? 0 55 56 return ( 57 <li 58 key={repo.name} 59 className="flex items-center gap-2 px-3 py-2 rounded" 60 style={{ 61 background: 'var(--bg-tertiary)', 62 border: '1px solid var(--border-color)', 63 }} 64 > 65 {/* Sync indicator dot */} 66 <span 67 className="w-3 h-3 rounded-full flex-shrink-0 transition-colors" 68 style={{ 69 background: color, 70 boxShadow: `0 0 6px ${color}`, 71 }} 72 title={title} 73 /> 74 {/* Repo name */} 75 <span 76 className="font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap flex-1" 77 style={{ color: 'var(--text-primary)' }} 78 > 79 {repo.name} 80 </span> 81 {/* Unmerged branches count */} 82 {unmerged > 0 && ( 83 <span 84 className="text-xs flex-shrink-0" 85 style={{ color: 'var(--text-secondary)' }} 86 title="Unmerged branches" 87 > 88 ({unmerged}) 89 </span> 90 )} 91 {/* CI status icon */} 92 {ciStatus && <CiStatusIcon status={ciStatus} />} 93 </li> 94 ) 95 })} 96 </ul> 97 )} 98 </div> 99 </section> 100 ) 101 }