/ frontend / src / components / overview / RepositoriesCard.tsx
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  }