feat: resolvers views and search fallback page

This commit is contained in:
thesn10
2026-06-25 12:38:41 +02:00
parent ef7dca34f9
commit 530ea63d36
6 changed files with 174 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
'use client'
import { use } from 'react'
import { ResolverDetail } from '@/components/ResolverDetail'
import type { Address } from '@solisting/sdk'
export default function ResolverPage({ params }: { params: Promise<{ pk: string }> }) {
const { pk } = use(params)
return <ResolverDetail pk={pk as Address} />
}

View File

@@ -0,0 +1,2 @@
import { ResolverGrid } from '@/components/ResolverGrid'
export default function ResolversPage() { return <ResolverGrid /> }

View File

@@ -0,0 +1,20 @@
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
import { Suspense } from 'react'
function SearchContent() {
const params = useSearchParams()
const router = useRouter()
const q = params.get('q') ?? ''
return (
<div style={{ maxWidth: 520, margin: '80px auto', textAlign: 'center' }}>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 12 }}>GLOBAL SEARCH</div>
<h1 style={{ margin: '0 0 10px', fontSize: 24, fontWeight: 700 }}>No account found</h1>
<p style={{ margin: '0 0 8px', fontSize: 14, color: 'var(--mut)', lineHeight: 1.55 }}>Nothing on-chain matched this query on the selected network.</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--acc2light)', wordBreak: 'break-all', background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 10, padding: '12px 16px', margin: '18px 0 24px' }}>{q}</div>
<button onClick={() => router.push('/listings')} style={{ padding: '11px 20px', borderRadius: 11, background: 'var(--bg3)', border: '1px solid var(--bd)', fontWeight: 600, fontSize: 13 }}>Back to Listings</button>
</div>
)
}
export default function SearchPage() {
return <Suspense><SearchContent /></Suspense>
}

View File

@@ -0,0 +1,67 @@
'use client'
import { useRouter } from 'next/navigation'
import { ResolverType, AcceptancePolicy } from '@descro/sdk'
import { useResolvers } from '@/hooks/useResolvers'
import { FieldRow, MonoChip } from '@/components/ui/FieldRow'
import { abbrev } from '@/lib/format'
import type { Address } from '@solisting/sdk'
export function ResolverDetail({ pk }: { pk: Address }) {
const router = useRouter()
const { data: resolvers = [], isLoading } = useResolvers()
const r = resolvers.find((x) => x.pda === pk)
if (isLoading) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading</div>
if (!r) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Resolver entry not found.</div>
const d = r.account
const total = Number(d.ruledForBuyer) + Number(d.ruledForSeller)
const buyerPct = total > 0 ? Math.round(Number(d.ruledForBuyer) / total * 100) : 0
const sellerPct = total > 0 ? 100 - buyerPct : 0
return (
<div>
<button onClick={() => router.push('/resolvers')} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--mut)', fontSize: 13, fontWeight: 500, marginBottom: 18 }}>
Resolvers
</button>
<div style={{ marginBottom: 8, fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)' }}>RESOLVER ENTRY · {abbrev(pk)}</div>
<h1 style={{ margin: '0 0 6px', fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>{d.name || abbrev(pk)}</h1>
{d.description && <p style={{ margin: '0 0 24px', fontSize: 14, color: 'var(--mut)', maxWidth: 620, lineHeight: 1.55 }}>{d.description}</p>}
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18, alignItems: 'start' }}>
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>ACCOUNT FIELDS</div>
<FieldRow label="Authority"><MonoChip value={abbrev(d.authority)} onCopy={() => navigator.clipboard.writeText(d.authority)} /></FieldRow>
<FieldRow label="Type"><span style={{ fontSize: 13, fontWeight: 600 }}>{ResolverType[d.resolverType as number] as string}</span></FieldRow>
<FieldRow label="Policy"><span style={{ fontSize: 13 }}>{AcceptancePolicy[d.acceptancePolicy as number] as string}</span></FieldRow>
<FieldRow label="Fee"><span style={{ fontSize: 13.5, fontWeight: 600 }}>{(d.feeBps / 100).toFixed(2)}%</span></FieldRow>
<FieldRow label="Fee Recipient"><MonoChip value={abbrev(d.feeRecipient)} onCopy={() => navigator.clipboard.writeText(d.feeRecipient)} /></FieldRow>
{d.metadataUri && <FieldRow label="Metadata"><a href={d.metadataUri} target="_blank" rel="noreferrer" style={{ fontSize: 12.5, color: 'var(--acc2light)', wordBreak: 'break-all' }}>{d.metadataUri}</a></FieldRow>}
</div>
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '20px 22px' }}>
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, marginBottom: 16 }}>DISPUTE STATISTICS</div>
<div style={{ textAlign: 'center', marginBottom: 18 }}>
<div style={{ fontSize: 34, fontWeight: 700 }}>{total}</div>
<div style={{ fontSize: 12, color: 'var(--mut)' }}>total resolved</div>
</div>
{[
{ label: 'Ruled for buyer', count: Number(d.ruledForBuyer), pct: buyerPct, color: 'var(--acc2)', barColor: 'var(--acc2)' },
{ label: 'Ruled for seller', count: Number(d.ruledForSeller), pct: sellerPct, color: '#F5B23E', barColor: '#F5B23E' },
].map((stat) => (
<div key={stat.label} style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, marginBottom: 6 }}>
<span style={{ color: stat.color, fontWeight: 600 }}>{stat.label}</span>
<span style={{ fontWeight: 600 }}>{stat.count} · {stat.pct}%</span>
</div>
<div style={{ height: 8, borderRadius: 5, background: 'var(--bg3)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${stat.pct}%`, background: stat.barColor }} />
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,60 @@
'use client'
import { useRouter } from 'next/navigation'
import { ResolverType, AcceptancePolicy } from '@descro/sdk'
import { useResolvers } from '@/hooks/useResolvers'
import { abbrev } from '@/lib/format'
export function ResolverGrid() {
const router = useRouter()
const { data: resolvers = [], isLoading } = useResolvers()
if (isLoading) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading</div>
return (
<div>
<div style={{ marginBottom: 22 }}>
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>Resolver Registry</h1>
<p style={{ margin: '6px 0 0', fontSize: 13, color: 'var(--mut)' }}>
{resolvers.length} ResolverEntry PDAs · descro_ext_resolvers program
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{resolvers.map((r) => {
const d = r.account
const total = Number(d.ruledForBuyer) + Number(d.ruledForSeller)
const buyerPct = total > 0 ? Math.round(Number(d.ruledForBuyer) / total * 100) : 0
const sellerPct = total > 0 ? 100 - buyerPct : 0
const feePct = (d.feeBps / 100).toFixed(2)
const resolverTypeName = ResolverType[d.resolverType as number] as string
const policyName = AcceptancePolicy[d.acceptancePolicy as number] as string
return (
<div
key={r.pda}
onClick={() => router.push(`/resolver/${r.pda}`)}
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '20px 22px', cursor: 'pointer' }}
>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, marginBottom: 14 }}>
<div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{d.name || abbrev(r.pda)}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--mut)', marginTop: 3 }}>{abbrev(r.pda)}</div>
</div>
<span style={{ padding: '4px 11px', borderRadius: 999, fontSize: 11, fontWeight: 600, color: 'var(--acc2light)', background: 'var(--accSoft)', flexShrink: 0 }}>{feePct}% fee</span>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
<span style={{ padding: '4px 10px', borderRadius: 7, fontSize: 11, fontWeight: 600, background: 'var(--bg3)', color: 'var(--mut)' }}>{resolverTypeName}</span>
<span style={{ padding: '4px 10px', borderRadius: 7, fontSize: 11, fontWeight: 600, background: 'var(--bg3)', color: 'var(--mut)' }}>{policyName}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, textAlign: 'center', borderTop: '1px solid var(--bdSoft)', paddingTop: 14 }}>
<div><div style={{ fontSize: 18, fontWeight: 700 }}>{total}</div><div style={{ fontSize: 10.5, color: 'var(--mut)', marginTop: 2 }}>resolved</div></div>
<div><div style={{ fontSize: 18, fontWeight: 700, color: 'var(--acc2light)' }}>{buyerPct}%</div><div style={{ fontSize: 10.5, color: 'var(--mut)', marginTop: 2 }}>for buyer</div></div>
<div><div style={{ fontSize: 18, fontWeight: 700, color: '#F5B23E' }}>{sellerPct}%</div><div style={{ fontSize: 10.5, color: 'var(--mut)', marginTop: 2 }}>for seller</div></div>
</div>
</div>
)
})}
{resolvers.length === 0 && <div style={{ gridColumn: '1/-1', padding: 48, textAlign: 'center', color: 'var(--mut)' }}>No resolvers registered.</div>}
</div>
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query'
import { useCluster } from '@solana/connector/react'
import { createSolanaRpc } from '@solana/kit'
import { fetchAllResolvers } from '@descro/sdk'
export function useResolvers() {
const { cluster } = useCluster()
return useQuery({
queryKey: ['resolvers', cluster?.id],
queryFn: () => {
const rpc = createSolanaRpc(cluster!.url)
return fetchAllResolvers(rpc as Parameters<typeof fetchAllResolvers>[0])
},
enabled: !!cluster,
refetchInterval: 30_000,
})
}