From 9c8001f21cfc2dadfd564a1d9ab27bb293cfdb14 Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:22:15 +0200 Subject: [PATCH] feat: listing detail view --- app/src/app/listing/[pk]/page.tsx | 18 +++ app/src/components/ListingDetail.tsx | 170 +++++++++++++++++++++++++++ app/src/hooks/useListing.ts | 18 +++ app/src/hooks/useOrdersForListing.ts | 18 +++ 4 files changed, 224 insertions(+) create mode 100644 app/src/app/listing/[pk]/page.tsx create mode 100644 app/src/components/ListingDetail.tsx create mode 100644 app/src/hooks/useListing.ts create mode 100644 app/src/hooks/useOrdersForListing.ts diff --git a/app/src/app/listing/[pk]/page.tsx b/app/src/app/listing/[pk]/page.tsx new file mode 100644 index 0000000..f16b441 --- /dev/null +++ b/app/src/app/listing/[pk]/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import { use } from 'react' +import { useWallet } from '@solana/connector/react' +import { ListingDetail } from '@/components/ListingDetail' +import type { Address } from '@solisting/sdk' + +export default function ListingPage({ params }: { params: Promise<{ pk: string }> }) { + const { pk } = use(params) + const { account } = useWallet() + + return ( + + ) +} diff --git a/app/src/components/ListingDetail.tsx b/app/src/components/ListingDetail.tsx new file mode 100644 index 0000000..f4e9b90 --- /dev/null +++ b/app/src/components/ListingDetail.tsx @@ -0,0 +1,170 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { isSome } from '@solana/kit' +import { useListing } from '@/hooks/useListing' +import { useOrdersForListing } from '@/hooks/useOrdersForListing' +import { Badge, STATUS_COLORS } from '@/components/ui/Badge' +import { FieldRow, MonoChip } from '@/components/ui/FieldRow' +import { Button } from '@/components/ui/Button' +import { abbrev, fmtSol, fmtTok } from '@/lib/format' +import type { Address } from '@solisting/sdk' + +interface Props { + pk: Address + walletAddress?: string | null + onUpdate?: () => void + onClose?: () => void + onPlaceOrder?: () => void +} + +export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrder }: Props) { + const router = useRouter() + const { data: listing, isLoading, error } = useListing(pk) + const { data: orders = [] } = useOrdersForListing(pk) + + if (isLoading) return
Loading…
+ if (error || !listing) return
Listing account not found.
+ + const d = listing.data + const avail = d.quantity - d.quantityReserved + const availPct = d.quantity > 0 ? `${(avail / d.quantity) * 100}%` : '0%' + const reservedPct = d.quantity > 0 ? `${(d.quantityReserved / d.quantity) * 100}%` : '0%' + const sc = STATUS_COLORS[d.isActive ? 'active' : 'inactive'] + + const isSeller = walletAddress && walletAddress === d.seller + const canPlace = !isSeller && d.isActive && avail > 0 && walletAddress + const canonicalOracleAddr = isSome(d.canonicalOracle) ? d.canonicalOracle.value : null + + function priceStr() { + const cur = d.canonicalCurrency + if (cur.__kind === 'Sol') return fmtSol(d.price) + return fmtTok(d.price, cur.decimals, 'SPL') + } + + return ( +
+ + +
+
+
+ LISTING ACCOUNT · #{String(d.listingId)} +
+

+ {d.metadataUri || abbrev(pk)} +

+
+ {d.isActive ? 'Active' : 'Inactive'} +
+ +
+ {/* Left: account fields */} +
+
ACCOUNT FIELDS
+ #{String(d.listingId)} + navigator.clipboard.writeText(d.seller)} onClick={() => router.push(`/search?q=${d.seller}`)} /> + {priceStr()} + + + {d.canonicalCurrency.__kind === 'Sol' ? 'SOL (native)' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`} + + + {canonicalOracleAddr && ( + + navigator.clipboard.writeText(canonicalOracleAddr)} /> + + )} + {d.isActive ? 'Active' : 'Inactive'} + + {d.metadataUri && ( + <> +
METADATA URI
+ + {d.metadataUri} + + + )} + +
ALT CURRENCIES
+ {d.altCurrencies.length === 0 + ?
None configured.
+ : d.altCurrencies.map((alt, i) => ( +
+ + {alt.currency.__kind === 'Sol' ? 'SOL' : `SPL · ${abbrev((alt.currency as { mint: string }).mint)}`} + + + {isSome(alt.usdOracle) ? abbrev(alt.usdOracle.value) : 'stablecoin ($1.00)'} + +
+ )) + } + +
ACCEPTED RESOLVERS
+ {d.acceptedResolvers.length === 0 + ?
Empty — any registered resolver is accepted.
+ : d.acceptedResolvers.map((r) => ( +
router.push(`/resolver/${r}`)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 13px', background: 'var(--bg3)', borderRadius: 9, marginBottom: 7, cursor: 'pointer' }}> + {abbrev(r)} +
+ )) + } +
+ + {/* Right: qty + actions */} +
+
+
QUANTITY
+
+
+
+
+
+
{avail}
available
+
{d.quantityReserved}
reserved
+
{d.quantity}
total
+
+
+ +
+
ACTIONS
+ {isSeller && ( +
+ + +
+ )} + {canPlace && } + {!walletAddress &&
Connect a wallet to place an order or manage this listing.
} + {walletAddress && !isSeller && !canPlace &&
No actions available — listing is inactive or out of stock.
} +
+
+
+ + {/* Orders table */} +
ORDERS ON THIS LISTING
+
+
+
ORDER
BUYER
AMOUNT
ORDER ID
ESCROW ID
+
+ {orders.length === 0 &&
No orders yet.
} + {orders.map((o) => ( +
router.push(`/order/${o.address}`)} + style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', gap: 12, padding: '14px 20px', borderBottom: '1px solid var(--bdSoft)', cursor: 'pointer', alignItems: 'center' }} + > +
{abbrev(o.address)}
+
{abbrev(o.data.buyer)}
+
{fmtSol(o.data.amount)}
+
#{String(o.data.orderId)}
+
#{String(o.data.escrowId)}
+
+ ))} +
+
+ ) +} diff --git a/app/src/hooks/useListing.ts b/app/src/hooks/useListing.ts new file mode 100644 index 0000000..30178ef --- /dev/null +++ b/app/src/hooks/useListing.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { useCluster } from '@solana/connector/react' +import { createSolanaRpc } from '@solana/kit' +import { fetchListing } from '@solisting/sdk' +import type { Address } from '@solisting/sdk' + +export function useListing(pk: Address) { + const { cluster } = useCluster() + return useQuery({ + queryKey: ['listing', pk, cluster?.id], + queryFn: () => { + const rpc = createSolanaRpc(cluster!.url) + return fetchListing(rpc, pk) + }, + enabled: !!cluster && !!pk, + refetchInterval: 15_000, + }) +} diff --git a/app/src/hooks/useOrdersForListing.ts b/app/src/hooks/useOrdersForListing.ts new file mode 100644 index 0000000..f562681 --- /dev/null +++ b/app/src/hooks/useOrdersForListing.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { useCluster } from '@solana/connector/react' +import { createSolanaRpc } from '@solana/kit' +import { fetchOrdersForListing } from '@solisting/sdk' +import type { Address } from '@solisting/sdk' + +export function useOrdersForListing(listingPk: Address) { + const { cluster } = useCluster() + return useQuery({ + queryKey: ['orders', listingPk, cluster?.id], + queryFn: () => { + const rpc = createSolanaRpc(cluster!.url) + return fetchOrdersForListing(rpc, listingPk) + }, + enabled: !!cluster && !!listingPk, + refetchInterval: 15_000, + }) +}