import React, { useEffect, useCallback } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { SubBar } from '../shell/SubBar'
import { Sub } from '../primitives'
import { FilterChips } from '../features/home/FilterChips'
import { SessionsTable } from '../features/home/SessionsTable'
import { useSessions } from '../features/home/useSessions'
import { useHomeCursor } from '../features/home/useHomeCursor'
import { useKeyboardCursor } from './__root'
import { AuthError, APIError } from '../api/client'
import type { HomeFilters } from '../features/home/useSessions'
import type { Session } from '../api/adapters'
import '../styles/home.css'
type HomeSearch = {
since?: '1d' | '7d' | '30d' | '90d' | 'all'
tool?: string
host?: string
}
const VALID_SINCE = new Set<string>(['1d', '7d', '30d', '90d', 'all'])
export const Route = createFileRoute('/')({
validateSearch: (search: Record<string, unknown>): HomeSearch => {
const since = typeof search['since'] === 'string' && VALID_SINCE.has(search['since'])
? (search['since'] as HomeSearch['since'])
: undefined
const tool = typeof search['tool'] === 'string' && search['tool'] !== '' ? search['tool'] : undefined
const host = typeof search['host'] === 'string' && search['host'] !== '' ? search['host'] : undefined
return { since, tool, host }
},
component: HomeRoute,
})
function HomeRoute(): React.JSX.Element {
const navigate = useNavigate()
const search = Route.useSearch()
const filters: HomeFilters = {
since: search.since,
tool: search.tool,
host: search.host,
}
const { data: sessions, isLoading, error } = useSessions(filters)
const handleOpen = useCallback((s: Session) => {
void navigate({ to: '/session/$tool/$host/$id', params: { tool: s.tool, host: s.host, id: s.id } })
}, [navigate])
const { cursor, move, activate, jumpTo } = useHomeCursor(
sessions?.length ?? 0,
useCallback((idx: number) => {
if (sessions && sessions[idx]) handleOpen(sessions[idx])
}, [sessions, handleOpen]),
)
// Wire our cursor into the global keyboard controller
const cursorRef = useKeyboardCursor()
useEffect(() => {
cursorRef.current = { move, activate }
return () => { cursorRef.current = { move: (_d: 1 | -1) => { /* no-op */ }, activate: () => { /* no-op */ } } }
}, [cursorRef, move, activate])
function handleFilterChange(next: HomeFilters) {
void navigate({
to: '/',
search: {
since: next.since,
tool: next.tool,
host: next.host,
},
})
}
if (isLoading) {
return (
<>
<SubBar>
<FilterChips value={filters} onChange={handleFilterChange} />
</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 sessions.</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>
<FilterChips value={filters} onChange={handleFilterChange} />
</SubBar>
<SessionsTable
sessions={sessions ?? []}
cursor={cursor}
onCursor={jumpTo}
onOpen={handleOpen}
/>
</>
)
}