Files
unified-media-manager/frontend/src/pages/Activity.tsx
2026-04-24 10:45:19 -07:00

161 lines
5.8 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import { fetchAPI } from '../api/client'
import Pagination from '../components/Pagination'
import ErrorBanner from '../components/ErrorBanner'
import Loading from '../components/Loading'
interface ActivityEvent {
id: number
event_type: string
media_id: number | null
media_type: string | null
title: string
description: string | null
data: Record<string, unknown>
created_at: string
}
interface PaginatedResponse {
data: ActivityEvent[]
total: number
page: number
page_size: number
total_pages: number
}
const PAGE_SIZE = 50
function eventTypeBadge(type: string): { label: string; className: string } {
switch (type) {
case 'grab':
return { label: 'Grab', className: 'bg-blue-600' }
case 'import':
return { label: 'Import', className: 'bg-green-600' }
case 'download_complete':
return { label: 'Download', className: 'bg-green-600' }
case 'download_failed':
return { label: 'Failed', className: 'bg-red-600' }
case 'quality_upgrade':
return { label: 'Upgrade', className: 'bg-purple-600' }
case 'safety_block':
return { label: 'Blocked', className: 'bg-orange-600' }
case 'error':
return { label: 'Error', className: 'bg-red-600' }
default:
return { label: 'Info', className: 'bg-gray-600' }
}
}
function formatTimeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime()
const minutes = Math.floor(diff / 60000)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
return `${days}d ago`
}
export default function Activity() {
const [events, setEvents] = useState<ActivityEvent[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [typeFilter, setTypeFilter] = useState('')
const fetchActivity = useCallback(() => {
setLoading(true)
setError(null)
let url = `/api/activity?page=${page}&page_size=${PAGE_SIZE}`
if (typeFilter) url += `&event_type=${typeFilter}`
fetchAPI<PaginatedResponse>(url)
.then(res => {
setEvents(res.data ?? [])
setTotal(res.total)
})
.catch(err => setError(err.message || 'Something went wrong. Please try again.'))
.finally(() => setLoading(false))
}, [page, typeFilter])
useEffect(() => {
fetchActivity()
}, [fetchActivity])
return (
<div>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold text-gray-100">Activity</h2>
<select
value={typeFilter}
onChange={e => { setTypeFilter(e.target.value); setPage(1) }}
className="bg-gray-800 border border-gray-700 text-gray-200 rounded px-3 py-2 text-sm"
>
<option value="">All Events</option>
<option value="grab">Grabs</option>
<option value="import">Imports</option>
<option value="download_complete">Downloads</option>
<option value="download_failed">Failures</option>
<option value="quality_upgrade">Upgrades</option>
<option value="safety_block">Safety Blocks</option>
<option value="error">Errors</option>
</select>
</div>
{error && <ErrorBanner error={error} onRetry={fetchActivity} />}
{loading ? (
<Loading />
) : events.length === 0 ? (
<div className="text-gray-500 text-center py-20 text-sm">No activity events</div>
) : (
<>
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-400 uppercase w-28">Type</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-400 uppercase">Title</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-400 uppercase w-24">Media</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-400 uppercase w-20">Time</th>
</tr>
</thead>
<tbody>
{events.map(event => {
const badge = eventTypeBadge(event.event_type)
return (
<tr key={event.id} className="border-b border-gray-800/50 hover:bg-gray-800/30">
<td className="px-4 py-3">
<span className={`inline-block px-2 py-0.5 rounded text-xs font-semibold text-white ${badge.className}`}>
{badge.label}
</span>
</td>
<td className="px-4 py-3">
<p className="text-sm text-gray-100 truncate max-w-md">{event.title}</p>
{event.description && (
<p className="text-xs text-gray-500 mt-0.5">{event.description}</p>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400">
{event.media_id != null ? `${event.media_type} #${event.media_id}` : '—'}
</td>
<td className="px-4 py-3 text-xs text-gray-500">{formatTimeAgo(event.created_at)}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<Pagination
total={total}
page={page}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
</div>
)
}