'use client' import { useState } from 'react' import useSWR from 'swr' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' 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 pnl_usd: string | null pnl_percent: string | null apr: string | null bonus_usd: string | null earned_usd: string | null liquidity_usd: string | null 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) } function formatUsd(value: string | null): string { if (!value) return '-' const n = parseFloat(value) if (isNaN(n)) return value return n >= 0 ? `$${n.toFixed(2)}` : `-$${Math.abs(n).toFixed(2)}` } export default function PositionsPage() { const { data: positions, mutate } = useSWR('/api/positions', fetcher, { refreshInterval: 5000 }) const [closingId, setClosingId] = useState(null) const [deletingId, setDeletingId] = useState(null) const [error, setError] = useState(null) const [confirmAction, setConfirmAction] = useState<{ type: 'close' | 'delete'; row: PositionRow } | null>(null) const [showClosed, setShowClosed] = useState(false) const [syncing, setSyncing] = useState(false) const [syncResult, setSyncResult] = useState(null) const allRows: PositionRow[] = positions || [] const rows = showClosed ? allRows : allRows.filter((r) => r.status !== 'closed') const handleSync = async () => { setSyncing(true) setSyncResult(null) setError(null) try { const res = await fetch('/api/sync', { method: 'POST' }) const data = await res.json() if (!res.ok) { setError(data.error || 'Sync failed') } else { setSyncResult(`Synced ${data.synced} positions, closed ${data.closed}, API total ${data.total}`) mutate() } } catch (e) { setError(e instanceof Error ? e.message : 'Sync failed') } finally { setSyncing(false) } } const handleClose = async (row: PositionRow) => { 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) => { 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) } } const handleConfirm = () => { if (!confirmAction) return if (confirmAction.type === 'close') { handleClose(confirmAction.row) } else { handleDelete(confirmAction.row) } setConfirmAction(null) } return (

Position Mappings

{error && (
{error}
)} {syncResult && (
{syncResult}
)} {rows.length === 0 ? (

No position mappings yet

) : (
Pool Size PNL Fee Bonus APR Price Range Target Our NFT Status Created Actions {rows.map((row) => { const pnlNum = parseFloat(row.pnl_usd || '0') const pnlColor = pnlNum > 0 ? 'text-green-400' : pnlNum < 0 ? 'text-red-400' : 'text-muted-foreground' return ( {row.pool_label || `${row.pool_id.slice(0, 6)}...`} {(row.liquidity_usd || row.size_usd) ? (
{formatUsd(row.liquidity_usd || row.size_usd)} {row.amount_a && row.symbol_a && (
{row.amount_a} {row.symbol_a} {row.amount_b && ` + ${row.amount_b} ${row.symbol_b}`}
)}
) : ( - )}
{row.pnl_usd ? (
{formatUsd(row.pnl_usd)} {row.pnl_percent && (
{parseFloat(row.pnl_percent) >= 0 ? '+' : ''}{parseFloat(row.pnl_percent).toFixed(2)}%
)}
) : ( - )}
0 ? 'text-green-400' : 'text-muted-foreground'}> {formatUsd(row.earned_usd)} 0 ? 'text-green-400' : 'text-muted-foreground'}> {formatUsd(row.bonus_usd)} {row.apr ? ( {row.apr}% ) : ( - )} {row.price_lower && row.price_upper ? `${formatPrice(row.price_lower)} ~ ${formatPrice(row.price_upper)}` : `${row.tick_lower} ~ ${row.tick_upper}`} {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)} {row.our_nft_mint ? ( {row.our_nft_mint.slice(0, 6)}... ) : ( pending )} {row.status} {new Date(row.created_at + 'Z').toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', })}
{row.status === 'active' && row.our_nft_mint && ( )}
) })}
)}
!open && setConfirmAction(null)}> {confirmAction?.type === 'close' ? 'Close Position' : 'Delete Position'} {confirmAction?.type === 'close' ? `Close position ${confirmAction.row.our_nft_mint?.slice(0, 8)}...? This will remove all liquidity on-chain and swap tokens back to USDC.` : `Delete position mapping #${confirmAction?.row.id}? This only removes the record from the database, not from the blockchain.`} Cancel {confirmAction?.type === 'close' ? 'Close Position' : 'Delete'}
) }