39246-vm/frontend/components/listings/ListingsTable.tsx
abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

95 lines
3.9 KiB
TypeScript

'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import ListingRow from './ListingRow'
import ListingDetailPanel from './ListingDetailPanel'
import { useListings, useDeleteAllListings } from '@/hooks/useListings'
import { getExportUrl } from '@/lib/api/listings'
import type { Listing } from '@/lib/types'
export default function ListingsTable() {
const { data: listings, isLoading, isError } = useListings()
const deleteAll = useDeleteAllListings()
const [selected, setSelected] = useState<Listing | null>(null)
const [search, setSearch] = useState('')
if (isLoading) return <SkeletonTable />
if (isError) return <ErrorBanner />
const filtered = search
? (listings ?? []).filter((l) =>
l.title.toLowerCase().includes(search.toLowerCase()) ||
l.keyword.toLowerCase().includes(search.toLowerCase())
)
: (listings ?? [])
return (
<>
<motion.div
initial={{ opacity: 1, y: -12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="g-card"
>
{/* Toolbar */}
<div className="flex gap-3 items-center flex-wrap px-5 py-4 border-b border-g-border/40">
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-g-faint/40 text-xs"></span>
<input
value={search} onChange={(e) => setSearch(e.target.value)}
placeholder="Search listings…"
className="g-input pl-7 w-56 h-9 text-sm"
/>
</div>
<span className="text-xs text-g-faint tabular-nums">{filtered.length} <span className="text-g-faint/50">lots</span></span>
<div className="ml-auto flex gap-2">
<button onClick={() => window.open(getExportUrl('csv'))} className="g-btn text-xs">Export CSV</button>
<button onClick={() => window.open(getExportUrl('json'))} className="g-btn text-xs">Export JSON</button>
<button onClick={() => { if (confirm('Clear all?')) deleteAll.mutate() }} className="g-btn-danger text-xs">Clear all</button>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="g-table">
<thead><tr>
<th className="w-16"></th><th>Title</th><th>Price</th>
<th>Time left</th><th className="text-center">Score</th>
<th>Keyword</th><th className="text-center">AI</th>
</tr></thead>
<tbody>
{filtered.map((l) => <ListingRow key={l.id} listing={l} onSelect={setSelected} />)}
</tbody>
</table>
{!filtered.length && (
<div className="flex flex-col items-center justify-center py-20 text-g-faint gap-3">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-g-raised to-g-panel flex items-center justify-center border border-g-border/30">
<span className="text-2xl opacity-30"></span>
</div>
<p className="text-sm font-medium">No listings captured yet</p>
<p className="text-xs text-g-faint/50">Start the engine and add target sites</p>
</div>
)}
</div>
</motion.div>
<ListingDetailPanel listing={selected} onClose={() => setSelected(null)} />
</>
)
}
const SkeletonTable = () => (
<div className="g-card p-5 space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} style={{ opacity: 1 - i * 0.15, animationDelay: `${i * 0.1}s` }}
className="h-14 bg-gradient-to-r from-g-raised/60 to-g-panel/30 rounded-xl animate-pulse" />
))}
</div>
)
const ErrorBanner = () => (
<div className="g-card border-g-red/20 p-5 text-g-red text-sm flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-g-red animate-pulse" />
Engine offline cannot reach server
</div>
)