小糯 🐱 f950654827 feat(ui): visual refresh — custom theme, refined layout, fixed bugs
- Custom color system (surface-0..4, accent, mint) replacing raw gray-xxx
- Inter + JetBrains Mono fonts via Google Fonts
- Refined sidebar: compact logo, geometric icons, subtle active states
- Fixed bg-gray-850 bug (invalid Tailwind class) in 9 components
- Polished login page with centered card + gradient logo
- Unified card/table/button/input styling across all components
- Subtle grain texture overlay for depth
- Smoother animations (fade-in, slide-up)
2026-04-13 17:56:39 +08:00

143 lines
5.5 KiB
TypeScript

import { useState, useEffect } from 'react'
import { getEvents } from '../api'
import { formatRelativeTime } from '../utils'
import { Spinner, EmptyState, HashBadge, Pagination } from './Common'
export default function Events() {
const [data, setData] = useState<Array<{ id: string; type_hash: string; payload: any; created_at: string }>>([])
const [expanded, setExpanded] = useState<Set<string>>(new Set())
const [refFilter, setRefFilter] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [limit, setLimit] = useState(50)
const [offset, setOffset] = useState(0)
const load = () => {
setLoading(true)
getEvents(refFilter || undefined, limit, offset)
.then((res) => {
setData(res.events)
setTotal(res.total)
})
.catch((e) => setError(e.message))
.finally(() => setLoading(false))
}
useEffect(() => {
load()
}, [limit, offset])
const handleSearch = () => {
setOffset(0) // reset to first page
load()
}
const toggleExpand = (id: string) => {
setExpanded((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}
if (loading) return <Spinner />
if (error) return <div className="text-red-500">Error: {error}</div>
return (
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-white tracking-tight">Events</h2>
<div className="flex gap-2">
<input
type="text"
className="px-4 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
placeholder="Filter by ref..."
value={refFilter}
onChange={(e) => setRefFilter(e.target.value)}
/>
<button
onClick={handleSearch}
className="px-4 py-2 bg-accent hover:bg-accent-dim rounded-lg text-sm font-medium transition-all"
>
Search
</button>
</div>
</div>
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? (
<EmptyState message="No events yet" />
) : (
<table className="w-full">
<thead className="bg-surface-3/80 border-b border-gray-700">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Type Hash
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Created
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Payload
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{data.map((event, i) => (
<tr
key={i}
className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-surface-3/60`}
>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{event.id}</td>
<td className="px-4 py-3">
<HashBadge hash={event.type_hash} />
</td>
<td className="px-4 py-3 text-sm text-gray-400">{formatRelativeTime(event.created_at)}</td>
<td className="px-4 py-3">
<button
onClick={() => toggleExpand(event.id)}
className="text-blue-400 hover:text-blue-300 text-sm font-medium transition-colors"
>
{expanded.has(event.id) ? 'Hide' : 'Show'}
</button>
{expanded.has(event.id) && (
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
{JSON.stringify(event.payload, null, 2)
.split('\n')
.map((line, i) => {
if (line.includes(':')) {
const [key, ...rest] = line.split(':')
return (
<div key={i}>
<span className="text-blue-400">{key}:</span>
<span className="text-green-300">{rest.join(':')}</span>
</div>
)
}
return (
<div key={i} className="text-gray-300">
{line}
</div>
)
})}
</pre>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div>
</div>
)
}