import React, { useEffect, useCallback } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { SubBar } from '../shell/SubBar'
import { Sub } from '../primitives'
import { ProjectsTable } from '../features/projects/ProjectsTable'
import { useProjects } from '../features/projects/useProjects'
import { useProjectsCursor } from '../features/projects/useProjectsCursor'
import { useKeyboardCursor } from './__root'
import { AuthError, APIError } from '../api/client'
import type { ProjectFilters } from '../features/projects/useProjects'
import type { Project } from '../api/adapters'
import '../styles/projects.css'
type ProjectsSearch = {
since?: '7d' | '30d' | '90d' | 'all'
}
const VALID_SINCE = new Set<string>(['7d', '30d', '90d', 'all'])
export const Route = createFileRoute('/projects')({
validateSearch: (search: Record<string, unknown>): ProjectsSearch => {
const since =
typeof search['since'] === 'string' && VALID_SINCE.has(search['since'])
? (search['since'] as ProjectsSearch['since'])
: undefined
return { since }
},
component: ProjectsRoute,
})
function SinceButtonGroup({
value,
onChange,
}: {
value: string
onChange: (v: ProjectsSearch['since']) => void
}): React.JSX.Element {
const options = ['7d', '30d', '90d', 'all'] as const
return (
<div className="since-btn-group">
{options.map(o => (
<button
key={o}
className={'since-btn' + (value === o ? ' active' : '')}
onClick={() => onChange(o)}
type="button"
>
{o}
</button>
))}
</div>
)
}
function ProjectsRoute(): React.JSX.Element {
const navigate = useNavigate()
const search = Route.useSearch()
const since = search.since ?? '30d'
const filters: ProjectFilters = { since }
const { data: projects, isLoading, error } = useProjects(filters)
const handleOpen = useCallback(
(p: Project) => {
// Route /project/$ is added in Phase 4; cast needed until routeTree is regenerated.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
void (navigate as any)({ to: '/project/$', params: { _splat: encodeURIComponent(p.cwd) } })
},
[navigate],
)
const { cursor, move, activate, jumpTo } = useProjectsCursor(
projects?.length ?? 0,
useCallback(
(idx: number) => {
if (projects && projects[idx]) handleOpen(projects[idx])
},
[projects, handleOpen],
),
)
const cursorRef = useKeyboardCursor()
useEffect(() => {
cursorRef.current = { move, activate }
return () => {
cursorRef.current = {
move: (_d: 1 | -1) => { /* no-op */ },
activate: () => { /* no-op */ },
}
}
}, [cursorRef, move, activate])
function handleSinceChange(next: ProjectsSearch['since']) {
void navigate({ to: '/projects', search: { since: next } })
}
const subBar = (
<SubBar>
<span className="mono muted">
{projects != null ? `${projects.length} projects` : '…'} · ranked by recent activity
</span>
<span style={{ flex: 1 }} />
<SinceButtonGroup value={since} onChange={handleSinceChange} />
</SubBar>
)
if (isLoading) {
return (
<>
{subBar}
<div className="body body-pad">
<Sub>loading…</Sub>
</div>
</>
)
}
if (error != null) {
if (error instanceof AuthError) {
return (
<div className="body body-pad" style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}>
<div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
<div className="uppercase-mono" style={{ marginBottom: 8 }}>not authenticated</div>
<div className="muted">Sign in to view your projects.</div>
</div>
</div>
)
}
const detail = error instanceof APIError ? error.message : String(error)
return (
<div className="body body-pad" style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}>
<div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
<div className="uppercase-mono" style={{ marginBottom: 8 }}>error</div>
<div className="muted">{detail}</div>
</div>
</div>
)
}
return (
<>
{subBar}
<ProjectsTable
projects={projects ?? []}
cursor={cursor}
onCursor={jumpTo}
onOpen={handleOpen}
/>
</>
)
}