From 5201de35ce34689d0c23f7fc282b6fe4607773c7 Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:16:09 +0200 Subject: [PATCH] feat: listings view with useListings hook and table --- app/src/app/listings/page.tsx | 5 ++ app/src/components/ListingsTable.tsx | 123 +++++++++++++++++++++++++++ app/src/hooks/useListings.ts | 17 ++++ 3 files changed, 145 insertions(+) create mode 100644 app/src/app/listings/page.tsx create mode 100644 app/src/components/ListingsTable.tsx create mode 100644 app/src/hooks/useListings.ts diff --git a/app/src/app/listings/page.tsx b/app/src/app/listings/page.tsx new file mode 100644 index 0000000..586bd8a --- /dev/null +++ b/app/src/app/listings/page.tsx @@ -0,0 +1,5 @@ +import { ListingsTable } from '@/components/ListingsTable' + +export default function ListingsPage() { + return +} diff --git a/app/src/components/ListingsTable.tsx b/app/src/components/ListingsTable.tsx new file mode 100644 index 0000000..7c1c722 --- /dev/null +++ b/app/src/components/ListingsTable.tsx @@ -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('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
Loading listings…
+ } + if (error) { + return
{String(error)}
+ } + + const FILTERS: { key: Filter; label: string }[] = [ + { key: 'all', label: 'All' }, + { key: 'active', label: 'Active' }, + { key: 'inactive', label: 'Inactive' }, + ] + + return ( +
+ {/* Header */} +
+
+

Listings

+

+ {listings.length} ListingAccount PDAs · solisting program +

+
+
+ 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' }} + /> +
+ {FILTERS.map((f) => ( + + ))} +
+
+
+ + {/* Table */} +
+
+
LISTING
SELLER
PRICE
AVAIL / TOTAL
ORDERS
STATUS
+
+ + {rows.length === 0 && ( +
+ No listings match these filters. +
+ )} + + {rows.map((l) => { + const avail = l.data.quantity - l.data.quantityReserved + const statusKey = l.data.isActive ? 'active' : 'inactive' + const sc = STATUS_COLORS[statusKey] + return ( +
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' }} + > +
+
+ {l.data.metadataUri || abbrev(l.address)} +
+
+ #{String(l.data.listingId)} · {abbrev(l.address)} +
+
+
+ {abbrev(l.data.seller)} +
+
{priceLabel(l.data)}
+
+ {avail} + / {l.data.quantity} +
+
{l.data.quantityReserved}
+
+ {l.data.isActive ? 'Active' : 'Inactive'} +
+
+ ) + })} +
+
+ ) +} diff --git a/app/src/hooks/useListings.ts b/app/src/hooks/useListings.ts new file mode 100644 index 0000000..3fd3a5b --- /dev/null +++ b/app/src/hooks/useListings.ts @@ -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, + }) +}