'use client' import PageHeading from './ui/PageHeading' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { authFetch, getApiBase, getToken, clearToken, getEventStreamToken } from './lib/auth' const normalizeRecentResults = (items: any[]) => items .filter((item: any) => item?.id) .map((item: any) => { const id = item.id const rawTitle = item.title const placeholder = typeof rawTitle === 'string' && rawTitle.trim().toLowerCase() === `request ${id}` return { id, title: !rawTitle || placeholder ? `Request #${id}` : rawTitle, year: item.year, type: item.type, statusLabel: item.statusLabel, artwork: item.artwork, createdAt: item.createdAt ?? null, } }) const REQUEST_STAGE_OPTIONS = [ { value: 'all', label: 'All stages' }, { value: 'pending', label: 'Waiting' }, { value: 'approved', label: 'Approved' }, { value: 'in_progress', label: 'In progress' }, { value: 'working', label: 'Working' }, { value: 'partial', label: 'Partial' }, { value: 'ready', label: 'Ready' }, { value: 'declined', label: 'Declined' }, ] export default function HomePage() { const router = useRouter() const [query, setQuery] = useState('') const [recent, setRecent] = useState< { id: number title: string year?: number type?: string statusLabel?: string artwork?: { poster_url?: string; backdrop_url?: string } createdAt?: string | null }[] >([]) const [recentError, setRecentError] = useState(null) const [recentLoading, setRecentLoading] = useState(false) const [searchResults, setSearchResults] = useState< { title: string year?: number type?: string requestId?: number statusLabel?: string requestedBy?: string | null accessible?: boolean }[] >([]) const [searchError, setSearchError] = useState(null) const [role, setRole] = useState(null) const [recentDays, setRecentDays] = useState(90) const [recentStage, setRecentStage] = useState('all') const [authReady, setAuthReady] = useState(false) const submit = (event: React.FormEvent) => { event.preventDefault() const trimmed = query.trim() if (!trimmed) return if (/^\d+$/.test(trimmed)) { router.push(`/requests/${encodeURIComponent(trimmed)}`) return } void runSearch(trimmed) } useEffect(() => { if (!getToken()) { router.push('/login') return } const load = async () => { setRecentLoading(true) setRecentError(null) try { const baseUrl = getApiBase() const meResponse = await authFetch(`${baseUrl}/auth/me`) if (!meResponse.ok) { if (meResponse.status === 401) { clearToken() router.push('/login') return } throw new Error(`Auth failed: ${meResponse.status}`) } const me = await meResponse.json() const userRole = me?.role ?? null setRole(userRole) setAuthReady(true) const take = userRole === 'admin' ? 50 : 6 const params = new URLSearchParams({ take: String(take), days: String(recentDays), }) if (recentStage !== 'all') { params.set('stage', recentStage) } const response = await authFetch(`${baseUrl}/requests/recent?${params.toString()}`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Recent requests failed: ${response.status}`) } const data = await response.json() if (Array.isArray(data?.results)) { setRecent(normalizeRecentResults(data.results)) } } catch (error) { console.error(error) setRecentError('Recent requests are not available right now.') } finally { setRecentLoading(false) } } load() }, [recentDays, recentStage]) useEffect(() => { if (!authReady) { return } if (!getToken()) { return } const baseUrl = getApiBase() let closed = false let source: EventSource | null = null const connect = async () => { try { const streamToken = await getEventStreamToken() if (closed) return const params = new URLSearchParams({ stream_token: streamToken, recent_days: String(recentDays), }) if (recentStage !== 'all') { params.set('recent_stage', recentStage) } const streamUrl = `${baseUrl}/events/stream?${params.toString()}` source = new EventSource(streamUrl) source.onmessage = (event) => { if (closed) return try { const payload = JSON.parse(event.data) if (!payload || typeof payload !== 'object') { return } if (payload.type === 'home_recent') { if (Array.isArray(payload.results)) { setRecent(normalizeRecentResults(payload.results)) setRecentError(null) setRecentLoading(false) } else if (typeof payload.error === 'string' && payload.error.trim()) { setRecentError('Recent requests are not available right now.') setRecentLoading(false) } return } } catch (error) { console.error(error) } } } catch (error) { if (closed) return console.error(error) } } void connect() return () => { closed = true source?.close() } }, [authReady, recentDays, recentStage]) const runSearch = async (term: string) => { try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/requests/search?query=${encodeURIComponent(term)}`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Search failed: ${response.status}`) } const data = await response.json() if (Array.isArray(data?.results)) { setSearchResults( data.results.map((item: any) => ({ title: item.title, year: item.year, type: item.type, requestId: item.requestId, statusLabel: item.statusLabel, requestedBy: item.requestedBy ?? null, accessible: Boolean(item.accessible), })) ) setSearchError(null) } } catch (error) { console.error(error) setSearchError('Search failed. Try a request ID instead.') setSearchResults([]) } } const resolveArtworkUrl = (url?: string | null) => { if (!url) return null return url.startsWith('http') ? url : `${getApiBase()}${url}` } const formatRequestTime = (value?: string | null) => { if (!value) return null const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } const activeRecentCount = recent.filter((item) => { const label = String(item.statusLabel ?? '').toLowerCase() return !label.includes('ready') && !label.includes('available') && !label.includes('declined') }).length const readyRecentCount = recent.filter((item) => { const label = String(item.statusLabel ?? '').toLowerCase() return label.includes('ready') || label.includes('available') }).length const requestCardState = (value?: string) => { const label = String(value ?? '').toLowerCase() if (label.includes('partial')) return { key: 'attention', label: value || 'Partially ready', progress: 65 } if (!/not |unavailable|waiting/.test(label) && (label.includes('ready') || label.includes('available'))) return { key: 'ready', label: value || 'Ready', progress: 100 } if (label.includes('declined') || label.includes('failed') || label.includes('error')) return { key: 'attention', label: value || 'Needs attention', progress: 12 } if (label.includes('working') || label.includes('progress') || label.includes('download')) return { key: 'processing', label: value || 'In progress', progress: 58 } return { key: 'waiting', label: value || 'Waiting', progress: 4 } } return (
setQuery(event.target.value)} placeholder="Dune 2021 or 1289" />
} /> {(searchError || searchResults.length > 0) && (
Search results

{searchError ? 'Search unavailable' : `${searchResults.length} match${searchResults.length === 1 ? '' : 'es'} found`}

{searchError ? (
{searchError}
) : (
{searchResults.map((item, index) => ( ))}
)}
)}
In view{recent.length}
In progress{activeRecentCount}
Ready{readyRecentCount}
Request activity

{role === 'admin' ? 'Recent requests' : 'My recent requests'}

{authReady && (
)}
{authReady && (
{REQUEST_STAGE_OPTIONS.filter((option) => ['all', 'pending', 'in_progress', 'working', 'ready'].includes(option.value)).map((option) => (