feat: listings view with useListings hook and table
This commit is contained in:
5
app/src/app/listings/page.tsx
Normal file
5
app/src/app/listings/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ListingsTable } from '@/components/ListingsTable'
|
||||
|
||||
export default function ListingsPage() {
|
||||
return <ListingsTable />
|
||||
}
|
||||
123
app/src/components/ListingsTable.tsx
Normal file
123
app/src/components/ListingsTable.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'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) => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setFilter(f.key)}
|
||||
style={{ padding: '6px 13px', borderRadius: 7, fontSize: 12.5, fontWeight: 600, color: filter === f.key ? 'var(--tx)' : 'var(--mut)', background: filter === f.key ? 'var(--bg2)' : 'transparent' }}
|
||||
>
|
||||
{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.metadataUri || 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>
|
||||
)
|
||||
}
|
||||
17
app/src/hooks/useListings.ts
Normal file
17
app/src/hooks/useListings.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useCluster } from '@solana/connector/react'
|
||||
import { createSolanaRpc } from '@solana/kit'
|
||||
import { fetchAllListings } from '@solisting/sdk'
|
||||
|
||||
export function useListings() {
|
||||
const { cluster } = useCluster()
|
||||
return useQuery({
|
||||
queryKey: ['listings', cluster?.id],
|
||||
queryFn: () => {
|
||||
const rpc = createSolanaRpc(cluster!.url)
|
||||
return fetchAllListings(rpc)
|
||||
},
|
||||
enabled: !!cluster,
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user