feat: dashboard with my listings and my orders
This commit is contained in:
5
app/src/app/dashboard/page.tsx
Normal file
5
app/src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Dashboard } from '@/components/Dashboard'
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return <Dashboard />
|
||||||
|
}
|
||||||
280
app/src/components/Dashboard.tsx
Normal file
280
app/src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useWallet } from '@solana/connector/react'
|
||||||
|
import { useMyListings } from '@/hooks/useMyListings'
|
||||||
|
import { useMyOrders } from '@/hooks/useMyOrders'
|
||||||
|
import { Badge, STATUS_COLORS } from '@/components/ui/Badge'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { abbrev, fmtSol } from '@/lib/format'
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
const router = useRouter()
|
||||||
|
const { account } = useWallet()
|
||||||
|
const [tab, setTab] = useState<'listings' | 'orders'>('listings')
|
||||||
|
const { data: myListings = [], isLoading: loadingL } = useMyListings()
|
||||||
|
const { data: myOrders = [], isLoading: loadingO } = useMyOrders()
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 420,
|
||||||
|
margin: '80px auto',
|
||||||
|
textAlign: 'center',
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
padding: '40px 32px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 14,
|
||||||
|
background: 'var(--accSoft)',
|
||||||
|
border: '1px solid var(--accBd)',
|
||||||
|
margin: '0 auto 18px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 5,
|
||||||
|
background: 'linear-gradient(135deg, var(--acc), var(--acc2))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 style={{ margin: '0 0 8px', fontSize: 20, fontWeight: 700 }}>Wallet required</h2>
|
||||||
|
<p style={{ margin: '0 0 22px', fontSize: 13.5, color: 'var(--mut)', lineHeight: 1.55 }}>
|
||||||
|
Connect a wallet to view your listings and orders.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<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' }}>
|
||||||
|
My Dashboard
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: '6px 0 0',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
color: 'var(--mut)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{abbrev(account)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => router.push('/listing/create')}>+ Create Listing</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 11,
|
||||||
|
padding: 4,
|
||||||
|
width: 'max-content',
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ key: 'listings' as const, label: `My Listings · ${myListings.length}` },
|
||||||
|
{ key: 'orders' as const, label: `My Orders · ${myOrders.length}` },
|
||||||
|
] as const
|
||||||
|
).map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.key}
|
||||||
|
onClick={() => setTab(t.key)}
|
||||||
|
style={{
|
||||||
|
padding: '9px 18px',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: tab === t.key ? 'var(--tx)' : 'var(--mut)',
|
||||||
|
background: tab === t.key ? 'var(--bg3)' : 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === 'listings' && (
|
||||||
|
<div>
|
||||||
|
{loadingL && (
|
||||||
|
<div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading…</div>
|
||||||
|
)}
|
||||||
|
{!loadingL && myListings.length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 48,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--mut)',
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You have no listings yet.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{myListings.map((l) => {
|
||||||
|
const sc = STATUS_COLORS[l.data.isActive ? 'active' : 'inactive']
|
||||||
|
const avail = l.data.quantity - l.data.quantityReserved
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={l.address}
|
||||||
|
style={{
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
padding: '18px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => router.push(`/listing/${l.address}`)}
|
||||||
|
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<span style={{ fontWeight: 600, fontSize: 15 }}>
|
||||||
|
{l.data.metadataUri || abbrev(l.address)}
|
||||||
|
</span>
|
||||||
|
<Badge color={sc.color} bg={sc.bg}>
|
||||||
|
{l.data.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--mut)',
|
||||||
|
marginTop: 4,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{String(l.data.listingId)} · {fmtSol(l.data.price)} · {avail}/
|
||||||
|
{String(l.data.quantity)} avail · {String(l.data.quantityReserved)} pending
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
style={{ padding: '8px 14px', fontSize: 12.5 }}
|
||||||
|
onClick={() => router.push(`/listing/create?edit=${l.address}`)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
style={{ padding: '8px 14px', fontSize: 12.5 }}
|
||||||
|
onClick={() => router.push(`/listing/${l.address}`)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'orders' && (
|
||||||
|
<div>
|
||||||
|
{loadingO && (
|
||||||
|
<div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading…</div>
|
||||||
|
)}
|
||||||
|
{!loadingO && myOrders.length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 48,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--mut)',
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You have no orders as a buyer.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{myOrders.map((o) => (
|
||||||
|
<div
|
||||||
|
key={o.address}
|
||||||
|
onClick={() => router.push(`/order/${o.address}`)}
|
||||||
|
style={{
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
padding: '18px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 18,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 15 }}>{abbrev(o.data.listing)}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--mut)',
|
||||||
|
marginTop: 4,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{String(o.data.orderId)} · {fmtSol(o.data.amount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '5px 13px',
|
||||||
|
borderRadius: 999,
|
||||||
|
background: 'var(--bg3)',
|
||||||
|
color: 'var(--mut)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{String(o.data.escrowId)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
app/src/hooks/useMyListings.ts
Normal file
8
app/src/hooks/useMyListings.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { useWallet } from '@solana/connector/react'
|
||||||
|
import { useListings } from './useListings'
|
||||||
|
|
||||||
|
export function useMyListings() {
|
||||||
|
const { account } = useWallet()
|
||||||
|
const { data: all = [], ...rest } = useListings()
|
||||||
|
return { data: all.filter((l) => l.data.seller === account), ...rest }
|
||||||
|
}
|
||||||
19
app/src/hooks/useMyOrders.ts
Normal file
19
app/src/hooks/useMyOrders.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useCluster, useWallet } from '@solana/connector/react'
|
||||||
|
import { createSolanaRpc } from '@solana/kit'
|
||||||
|
import { fetchOrdersByBuyer } from '@solisting/sdk'
|
||||||
|
import type { Address } from '@solisting/sdk'
|
||||||
|
|
||||||
|
export function useMyOrders() {
|
||||||
|
const { account } = useWallet()
|
||||||
|
const { cluster } = useCluster()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['myOrders', account, cluster?.id],
|
||||||
|
queryFn: () => {
|
||||||
|
const rpc = createSolanaRpc(cluster!.url)
|
||||||
|
return fetchOrdersByBuyer(rpc, account as Address)
|
||||||
|
},
|
||||||
|
enabled: !!account && !!cluster,
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user