| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- 'use client'
- import { useState } from 'react'
- import useSWR from 'swr'
- const fetcher = (url: string) => fetch(url).then((r) => r.json())
- interface PositionRow {
- id: number
- target_address: string
- target_nft_mint: string
- our_nft_mint: string | null
- pool_id: string
- pool_label: string
- tick_lower: number
- tick_upper: number
- price_lower: string
- price_upper: string
- size_usd: string
- amount_a: string
- amount_b: string
- symbol_a: string
- symbol_b: string
- status: string
- created_at: string
- }
- function formatPrice(price: string): string {
- if (!price) return ''
- const n = parseFloat(price)
- if (isNaN(n)) return price
- if (n >= 1000) return n.toFixed(2)
- if (n >= 1) return n.toFixed(4)
- if (n >= 0.0001) return n.toFixed(6)
- return n.toExponential(3)
- }
- export default function PositionsPage() {
- const { data: positions, mutate } = useSWR('/api/positions', fetcher, { refreshInterval: 5000 })
- const [closingId, setClosingId] = useState<number | null>(null)
- const [deletingId, setDeletingId] = useState<number | null>(null)
- const [error, setError] = useState<string | null>(null)
- const rows: PositionRow[] = positions || []
- const handleClose = async (row: PositionRow) => {
- if (
- !confirm(
- `Close position ${row.our_nft_mint?.slice(0, 8)}...? This will remove all liquidity on-chain and swap tokens back to USDC.`,
- )
- ) {
- return
- }
- setClosingId(row.id)
- setError(null)
- try {
- const res = await fetch('/api/positions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: row.id }),
- })
- const data = await res.json()
- if (!res.ok) {
- setError(data.error || 'Close failed')
- } else {
- mutate()
- }
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Close failed')
- } finally {
- setClosingId(null)
- }
- }
- const handleDelete = async (row: PositionRow) => {
- if (
- !confirm(
- `Delete position mapping #${row.id}? This only removes the record from the database, not from the blockchain.`,
- )
- ) {
- return
- }
- setDeletingId(row.id)
- setError(null)
- try {
- const res = await fetch(`/api/positions?id=${row.id}`, { method: 'DELETE' })
- const data = await res.json()
- if (!res.ok) {
- setError(data.error || 'Delete failed')
- } else {
- mutate()
- }
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Delete failed')
- } finally {
- setDeletingId(null)
- }
- }
- return (
- <div className="space-y-6">
- <h2 className="text-lg font-semibold">Position Mappings</h2>
- {error && (
- <div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-xs text-red-400">
- {error}
- <button onClick={() => setError(null)} className="ml-2 underline">
- dismiss
- </button>
- </div>
- )}
- <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
- {rows.length === 0 ? (
- <p className="text-xs text-zinc-500">No position mappings yet</p>
- ) : (
- <div className="overflow-x-auto">
- <table className="w-full text-xs">
- <thead>
- <tr className="text-zinc-500 border-b border-[var(--border)]">
- <th className="text-left py-2 pr-3">Target</th>
- <th className="text-left py-2 pr-3">Target NFT</th>
- <th className="text-left py-2 pr-3">Our NFT</th>
- <th className="text-left py-2 pr-3">Pool</th>
- <th className="text-left py-2 pr-3">Size</th>
- <th className="text-left py-2 pr-3">Price Range</th>
- <th className="text-left py-2 pr-3">Status</th>
- <th className="text-left py-2 pr-3">Created</th>
- <th className="text-left py-2">Actions</th>
- </tr>
- </thead>
- <tbody>
- {rows.map((row) => (
- <tr key={row.id} className="border-b border-zinc-800/50">
- <td className="py-2 pr-3 text-zinc-400">
- {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)}
- </td>
- <td className="py-2 pr-3">
- <a
- href={`https://solscan.io/token/${row.target_nft_mint}`}
- target="_blank"
- rel="noopener noreferrer"
- className="text-indigo-400 hover:underline"
- >
- {row.target_nft_mint.slice(0, 6)}...
- </a>
- </td>
- <td className="py-2 pr-3">
- {row.our_nft_mint ? (
- <a
- href={`https://solscan.io/token/${row.our_nft_mint}`}
- target="_blank"
- rel="noopener noreferrer"
- className="text-indigo-400 hover:underline"
- >
- {row.our_nft_mint.slice(0, 6)}...
- </a>
- ) : (
- <span className="text-zinc-500">pending</span>
- )}
- </td>
- <td className="py-2 pr-3 text-zinc-400">
- {row.pool_label || `${row.pool_id.slice(0, 6)}...`}
- </td>
- <td className="py-2 pr-3">
- {row.size_usd ? (
- <div>
- <span className="text-green-400 font-medium">${row.size_usd}</span>
- <div className="text-zinc-500 text-[10px] leading-tight mt-0.5">
- {row.amount_a && row.symbol_a && `${row.amount_a} ${row.symbol_a}`}
- {row.amount_a && row.amount_b && ' + '}
- {row.amount_b && row.symbol_b && `${row.amount_b} ${row.symbol_b}`}
- </div>
- </div>
- ) : (
- <span className="text-zinc-500">-</span>
- )}
- </td>
- <td className="py-2 pr-3 text-zinc-400">
- {row.price_lower && row.price_upper
- ? `${formatPrice(row.price_lower)} ~ ${formatPrice(row.price_upper)}`
- : `${row.tick_lower} ~ ${row.tick_upper}`}
- </td>
- <td className="py-2 pr-3">
- <span
- className={`px-1.5 py-0.5 rounded text-[10px] ${
- row.status === 'active'
- ? 'bg-green-500/20 text-green-400'
- : row.status === 'closed'
- ? 'bg-zinc-500/20 text-zinc-400'
- : 'bg-red-500/20 text-red-400'
- }`}
- >
- {row.status}
- </span>
- </td>
- <td className="py-2 pr-3 text-zinc-500">
- {new Date(row.created_at + 'Z').toLocaleDateString()}
- </td>
- <td className="py-2">
- <div className="flex gap-1.5">
- {row.status === 'active' && row.our_nft_mint && (
- <button
- onClick={() => handleClose(row)}
- disabled={closingId === row.id}
- className="px-2 py-0.5 text-[10px] rounded bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 disabled:opacity-50 transition-colors"
- >
- {closingId === row.id ? 'Closing...' : 'Close'}
- </button>
- )}
- <button
- onClick={() => handleDelete(row)}
- disabled={deletingId === row.id}
- className="px-2 py-0.5 text-[10px] rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-50 transition-colors"
- >
- {deletingId === row.id ? 'Deleting...' : 'Delete'}
- </button>
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- </div>
- )
- }
|