2025-05-23 19:48:23 +03:00
< script lang = "ts" >
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
2025-05-27 16:19:57 +03:00
import DataTable from '$lib/components/self/DataTable.svelte';
import PortfolioSkeleton from '$lib/components/self/skeletons/PortfolioSkeleton.svelte';
2025-05-24 15:50:10 +03:00
import { getPublicUrl , formatPrice , formatValue , formatQuantity , formatDate } from '$lib/utils';
2025-05-23 19:48:23 +03:00
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { TrendingUp , DollarSign , Wallet , TrendingDown , Clock , Receipt } from 'lucide-svelte';
import { goto } from '$app/navigation';
let { data } = $props();
let portfolioData = $state< any > (null);
let transactions = $state< any [ ] > ([]);
let loading = $state(true);
onMount(async () => {
await Promise.all([fetchPortfolioData(), fetchRecentTransactions()]);
});
async function fetchPortfolioData() {
try {
const response = await fetch('/api/portfolio/total');
if (response.ok) {
portfolioData = await response.json();
} else {
toast.error('Failed to load portfolio data');
}
} catch (e) {
console.error('Failed to fetch portfolio data:', e);
toast.error('Failed to load portfolio data');
}
}
async function fetchRecentTransactions() {
try {
const response = await fetch('/api/transactions');
if (response.ok) {
const result = await response.json();
transactions = result.transactions.slice(0, 10); // Show last 10 transactions
} else {
toast.error('Failed to load transactions');
}
} catch (e) {
console.error('Failed to fetch transactions:', e);
toast.error('Failed to load transactions');
} finally {
loading = false;
}
}
let totalPortfolioValue = $derived(portfolioData ? portfolioData.totalValue : 0);
let hasHoldings = $derived(portfolioData & & portfolioData.coinHoldings.length > 0);
let hasTransactions = $derived(transactions.length > 0);
2025-05-27 16:19:57 +03:00
let holdingsColumns = $derived([
{
key: 'coin',
label: 'Coin',
class: 'w-[30%] min-w-[120px] md:w-[12%]',
render: (value: any, row: any) => ({
component: 'coin',
icon: row.icon,
symbol: row.symbol,
name: `*${ row . symbol } `,
size: 6
})
},
{
key: 'quantity',
label: 'Quantity',
class: 'w-[20%] min-w-[80px] md:w-[12%] font-mono',
render: (value: any) => formatQuantity(value)
},
{
key: 'currentPrice',
label: 'Price',
class: 'w-[15%] min-w-[70px] md:w-[12%] font-mono',
render: (value: any) => `$${ formatPrice ( value )} `
},
{
key: 'change24h',
label: '24h Change',
class: 'w-[20%] min-w-[80px] md:w-[12%]',
render: (value: any) => ({
component: 'badge',
variant: value >= 0 ? 'success' : 'destructive',
text: `${ value >= 0 ? '+' : '' } ${ value . toFixed ( 2 )} %`
})
},
{
key: 'value',
label: 'Value',
class: 'w-[15%] min-w-[70px] md:w-[12%] font-mono font-medium',
render: (value: any) => formatValue(value)
},
{
key: 'portfolioPercent',
label: 'Portfolio %',
class: 'hidden md:table-cell md:w-[12%]',
render: (value: any, row: any) => ({
component: 'badge',
variant: 'outline',
text: `${(( row . value / totalPortfolioValue ) * 100 ). toFixed ( 1 )} %`
})
}
]);
// Column configurations for transactions table
let transactionsColumns = $derived([
{
key: 'type',
label: 'Type',
class: 'w-[15%] min-w-[60px] md:w-[10%]',
render: (value: any) => ({
component: 'badge',
variant: value === 'BUY' ? 'success' : 'destructive',
text: value === 'BUY' ? 'Buy' : 'Sell',
class: 'text-xs'
})
},
{
key: 'coin',
label: 'Coin',
class: 'w-[30%] min-w-[100px] md:w-[20%]',
render: (value: any, row: any) => ({
component: 'coin',
icon: row.coin.icon,
symbol: row.coin.symbol,
name: `*${ row . coin . symbol } `,
size: 4
})
},
{
key: 'quantity',
label: 'Quantity',
class: 'w-[20%] min-w-[80px] md:w-[15%] font-mono text-sm',
render: (value: any) => formatQuantity(value)
},
{
key: 'pricePerCoin',
label: 'Price',
class: 'w-[15%] min-w-[70px] md:w-[15%] font-mono text-sm',
render: (value: any) => `$${ formatPrice ( value )} `
},
{
key: 'totalBaseCurrencyAmount',
label: 'Total',
class: 'w-[20%] min-w-[70px] md:w-[15%] font-mono text-sm font-medium',
render: (value: any) => formatValue(value)
},
{
key: 'timestamp',
label: 'Date',
class: 'hidden md:table-cell md:w-[25%] text-muted-foreground text-sm',
render: (value: any) => formatDate(value)
}
]);
2025-05-23 19:48:23 +03:00
< / script >
< svelte:head >
< title > Portfolio - Rugplay< / title >
< / svelte:head >
< div class = "container mx-auto max-w-7xl p-6" >
< header class = "mb-8" >
< div >
< h1 class = "text-3xl font-bold" > Portfolio< / h1 >
< p class = "text-muted-foreground" > View your holdings and portfolio performance< / p >
< / div >
< / header >
{ #if loading }
2025-05-27 16:19:57 +03:00
< PortfolioSkeleton / >
2025-05-23 19:48:23 +03:00
{ :else if ! portfolioData }
< div class = "flex h-96 items-center justify-center" >
< div class = "text-center" >
< div class = "text-muted-foreground mb-4 text-xl" > Failed to load portfolio< / div >
< Button onclick = { fetchPortfolioData } > Try Again </ Button >
< / div >
< / div >
{ : else }
<!-- Portfolio Summary Cards -->
< div class = "mb-8 grid grid-cols-1 gap-4 lg:grid-cols-3" >
<!-- Total Portfolio Value -->
< Card.Root class = "text-success gap-1" >
< Card.Header >
< Card.Title class = "flex items-center gap-2 text-sm font-medium" >
< Wallet class = "h-4 w-4" / >
Total
< / Card.Title >
< / Card.Header >
< Card.Content >
< p class = "text-3xl font-bold" > { formatValue ( totalPortfolioValue )} </ p >
< / Card.Content >
< / Card.Root >
<!-- Base Currency Balance -->
< Card.Root class = "gap-1" >
< Card.Header >
< Card.Title class = "flex items-center gap-2 text-sm font-medium" >
< DollarSign class = "h-4 w-4" / >
Cash Balance
< / Card.Title >
< / Card.Header >
< Card.Content >
< p class = "text-3xl font-bold" >
{ formatValue ( portfolioData . baseCurrencyBalance )}
< / p >
< p class = "text-muted-foreground text-xs" >
{ totalPortfolioValue > 0
? `${(( portfolioData . baseCurrencyBalance / totalPortfolioValue ) * 100 ). toFixed ( 1 )} % of portfolio`
: '100% of portfolio'}
< / p >
< / Card.Content >
< / Card.Root >
<!-- Coin Holdings Value -->
< Card.Root class = "gap-1" >
< Card.Header >
< Card.Title class = "flex items-center gap-2 text-sm font-medium" >
< TrendingUp class = "h-4 w-4" / >
Coin Holdings
< / Card.Title >
< / Card.Header >
< Card.Content >
< p class = "text-3xl font-bold" > { formatValue ( portfolioData . totalCoinValue )} </ p >
< p class = "text-muted-foreground text-xs" >
{ portfolioData . coinHoldings . length } positions
< / p >
< / Card.Content >
< / Card.Root >
< / div >
{ #if ! hasHoldings }
<!-- Empty State -->
< Card.Root >
< Card.Content class = "py-16 text-center" >
< div
class="bg-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
>
< Wallet class = "text-muted-foreground h-8 w-8" / >
< / div >
< h3 class = "mb-2 text-lg font-semibold" > No coin holdings< / h3 >
< p class = "text-muted-foreground mb-6" >
You haven't invested in any coins yet. Start by buying existing coins.
< / p >
< div class = "flex justify-center" >
< Button variant = "outline" onclick = {() => goto ( '/' )} > Browse Coins </ Button >
< / div >
< / Card.Content >
< / Card.Root >
{ : else }
<!-- Holdings Table -->
< Card.Root >
< Card.Header >
< Card.Title > Your Holdings< / Card.Title >
< Card.Description > Current positions in your portfolio< / Card.Description >
< / Card.Header >
< Card.Content >
2025-05-27 16:19:57 +03:00
< DataTable
columns={ holdingsColumns }
data={ portfolioData . coinHoldings }
onRowClick={( holding ) => goto ( `/coin/$ { holding . symbol } `) }
/>
2025-05-23 19:48:23 +03:00
< / Card.Content >
< / Card.Root >
{ /if }
<!-- Recent Transactions -->
< Card.Root class = "mt-8" >
< Card.Header >
< div class = "flex items-center justify-between" >
< div >
< Card.Title class = "flex items-center gap-2" >
< Receipt class = "h-5 w-5" / >
Recent Transactions
< / Card.Title >
< Card.Description > Your latest trading activity< / Card.Description >
< / div >
{ #if hasTransactions }
< Button variant = "outline" size = "sm" onclick = {() => goto ( '/transactions' )} >
View All
< / Button >
{ /if }
< / div >
< / Card.Header >
< Card.Content >
2025-05-27 16:19:57 +03:00
< DataTable
columns={ transactionsColumns }
data={ transactions }
onRowClick={( tx ) => goto ( `/coin/$ { tx . coin . symbol } `) }
emptyIcon={ Receipt }
emptyTitle="No transactions yet"
emptyDescription="You haven't made any trades yet. Start by buying or selling coins."
/>
2025-05-23 19:48:23 +03:00
< / Card.Content >
< / Card.Root >
{ /if }
< / div >