136 lines
5.9 KiB
TypeScript
136 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useListings } from '@/hooks/useListings'
|
|
import { Badge, STATUS_COLORS } from '@/components/ui/Badge'
|
|
import { abbrev, fmtSol, fmtTok } from '@/lib/format'
|
|
import type { ListingAccountWithPda } from '@solisting/sdk'
|
|
|
|
type Filter = 'all' | 'active' | 'inactive'
|
|
|
|
function priceLabel(listing: ListingAccountWithPda['data']): string {
|
|
const cur = listing.canonicalCurrency
|
|
if (cur.__kind === 'Sol') return fmtSol(listing.price)
|
|
return fmtTok(listing.price, cur.decimals, 'SPL')
|
|
}
|
|
|
|
export function ListingsTable() {
|
|
const router = useRouter()
|
|
const { data: listings = [], isLoading, error } = useListings()
|
|
const [filter, setFilter] = useState<Filter>('all')
|
|
const [sellerFilter, setSellerFilter] = useState('')
|
|
|
|
const rows = listings.filter((l) => {
|
|
if (filter === 'active' && !l.data.isActive) return false
|
|
if (filter === 'inactive' && l.data.isActive) return false
|
|
if (sellerFilter && !l.data.seller.toLowerCase().includes(sellerFilter.toLowerCase())) return false
|
|
return true
|
|
})
|
|
|
|
if (isLoading) {
|
|
return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading listings…</div>
|
|
}
|
|
if (error) {
|
|
return <div style={{ padding: 48, textAlign: 'center', color: 'var(--danger)' }}>{String(error)}</div>
|
|
}
|
|
|
|
const FILTERS: { key: Filter; label: string }[] = [
|
|
{ key: 'all', label: 'All' },
|
|
{ key: 'active', label: 'Active' },
|
|
{ key: 'inactive', label: 'Inactive' },
|
|
]
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 16, marginBottom: 22, flexWrap: 'wrap' }}>
|
|
<div>
|
|
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>Listings</h1>
|
|
<p style={{ margin: '6px 0 0', fontSize: 13, color: 'var(--mut)' }}>
|
|
{listings.length} ListingAccount PDAs · solisting program
|
|
</p>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
|
<input
|
|
value={sellerFilter}
|
|
onChange={(e) => setSellerFilter(e.target.value)}
|
|
placeholder="Filter by seller address"
|
|
style={{ height: 38, width: 220, padding: '0 13px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, fontSize: 13, fontFamily: 'var(--font-mono)', outline: 'none' }}
|
|
/>
|
|
<div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}>
|
|
{FILTERS.map((f) => {
|
|
const active = filter === f.key
|
|
return (
|
|
<button
|
|
key={f.key}
|
|
onClick={() => setFilter(f.key)}
|
|
style={{
|
|
padding: '6px 13px',
|
|
borderRadius: 7,
|
|
fontSize: 12.5,
|
|
fontWeight: 600,
|
|
color: active ? '#0b0613' : 'var(--mut)',
|
|
background: active ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{f.label}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '2.4fr 1.1fr 1fr 1.1fr .8fr .9fr', gap: 14, padding: '13px 20px', borderBottom: '1px solid var(--bd)', fontSize: 10.5, fontWeight: 600, letterSpacing: '.12em', color: 'var(--mut)' }}>
|
|
<div>LISTING</div><div>SELLER</div><div>PRICE</div><div>AVAIL / TOTAL</div><div>ORDERS</div><div style={{ textAlign: 'right' }}>STATUS</div>
|
|
</div>
|
|
|
|
{rows.length === 0 && (
|
|
<div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)', fontSize: 14 }}>
|
|
No listings match these filters.
|
|
</div>
|
|
)}
|
|
|
|
{rows.map((l) => {
|
|
const avail = l.data.quantity - l.data.quantityReserved
|
|
const statusKey = l.data.isActive ? 'active' : 'inactive'
|
|
const sc = STATUS_COLORS[statusKey]
|
|
return (
|
|
<div
|
|
key={l.address}
|
|
onClick={() => router.push(`/listing/${l.address}`)}
|
|
style={{ display: 'grid', gridTemplateColumns: '2.4fr 1.1fr 1fr 1.1fr .8fr .9fr', gap: 14, padding: '15px 20px', borderBottom: '1px solid var(--bdSoft)', cursor: 'pointer', alignItems: 'center' }}
|
|
>
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{l.data.name || abbrev(l.address)}
|
|
</div>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}>
|
|
#{String(l.data.listingId)} · {abbrev(l.address)}
|
|
</div>
|
|
</div>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--acc2light)' }}>
|
|
{abbrev(l.data.seller)}
|
|
</div>
|
|
<div style={{ fontWeight: 600, fontSize: 13.5 }}>{priceLabel(l.data)}</div>
|
|
<div style={{ fontSize: 13 }}>
|
|
<span style={{ fontWeight: 600 }}>{avail}</span>
|
|
<span style={{ color: 'var(--mut)' }}> / {l.data.quantity}</span>
|
|
</div>
|
|
<div style={{ fontSize: 13, color: 'var(--mut)' }}>{l.data.quantityReserved}</div>
|
|
<div style={{ textAlign: 'right' }}>
|
|
<Badge color={sc.color} bg={sc.bg}>{l.data.isActive ? 'Active' : 'Inactive'}</Badge>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|