feat: p&l label in portfolio + fixed unnecessary calls to /total
This commit is contained in:
parent
8cba222fe2
commit
99614f853e
10 changed files with 172 additions and 53 deletions
|
|
@ -34,7 +34,7 @@
|
|||
import { mode, setMode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
||||
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
|
||||
import DailyRewards from './DailyRewards.svelte';
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
import { formatValue, getPublicUrl } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import { liveTradesStore, isLoadingTrades } from '$lib/stores/websocket';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
|
|
@ -64,11 +65,11 @@
|
|||
let showPromoCode = $state(false);
|
||||
let showUserManual = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
if ($USER_DATA) {
|
||||
fetchPortfolioData();
|
||||
fetchPortfolioSummary();
|
||||
} else {
|
||||
PORTFOLIO_DATA.set(null);
|
||||
PORTFOLIO_SUMMARY.set(null);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -208,7 +209,7 @@
|
|||
<Sidebar.Group>
|
||||
<Sidebar.GroupContent>
|
||||
<div class="px-2 py-1">
|
||||
{#if !$PORTFOLIO_DATA}
|
||||
{#if !$PORTFOLIO_SUMMARY}
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-8 w-full rounded" />
|
||||
</div>
|
||||
|
|
@ -318,7 +319,7 @@
|
|||
<Sidebar.GroupLabel>Portfolio</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupContent>
|
||||
<div class="space-y-2 px-2 py-1">
|
||||
{#if !$PORTFOLIO_DATA}
|
||||
{#if !$PORTFOLIO_SUMMARY}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="h-4 w-4 rounded" />
|
||||
|
|
@ -343,19 +344,19 @@
|
|||
<span class="text-sm font-medium">Total Value</span>
|
||||
</div>
|
||||
<Badge variant="secondary" class="font-mono">
|
||||
${formatCurrency($PORTFOLIO_DATA.totalValue)}
|
||||
${formatCurrency($PORTFOLIO_SUMMARY.totalValue)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>Cash:</span>
|
||||
<span class="font-mono"
|
||||
>${formatCurrency($PORTFOLIO_DATA.baseCurrencyBalance)}</span
|
||||
>${formatCurrency($PORTFOLIO_SUMMARY.baseCurrencyBalance)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Coins:</span>
|
||||
<span class="font-mono">${formatCurrency($PORTFOLIO_DATA.totalCoinValue)}</span>
|
||||
<span class="font-mono">${formatCurrency($PORTFOLIO_SUMMARY.totalCoinValue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Button } from '$lib/components/ui/button';
|
||||
import { Gift, Clock, Loader2, CheckIcon } from 'lucide-svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { formatTimeRemaining } from '$lib/utils';
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
});
|
||||
|
||||
if ($USER_DATA) {
|
||||
await fetchPortfolioData();
|
||||
await fetchPortfolioSummary();
|
||||
}
|
||||
|
||||
await fetchRewardStatus();
|
||||
|
|
|
|||
|
|
@ -1,26 +1,55 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface PortfolioData {
|
||||
export interface PortfolioSummary {
|
||||
baseCurrencyBalance: number;
|
||||
totalCoinValue: number;
|
||||
totalValue: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface PortfolioData extends PortfolioSummary {
|
||||
coinHoldings: Array<{
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
currentPrice: number;
|
||||
value: number;
|
||||
percentageChange: number;
|
||||
change24h: number;
|
||||
icon?: string;
|
||||
portfolioPercent: number;
|
||||
}>;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export const PORTFOLIO_SUMMARY = writable<PortfolioSummary | null>(null);
|
||||
|
||||
export const PORTFOLIO_DATA = writable<PortfolioData | null>(null);
|
||||
|
||||
export async function fetchPortfolioSummary() {
|
||||
try {
|
||||
const response = await fetch('/api/portfolio/summary');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
PORTFOLIO_SUMMARY.set(data);
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch portfolio summary:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function fetchPortfolioData() {
|
||||
try {
|
||||
const response = await fetch('/api/portfolio/total');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
PORTFOLIO_DATA.set(data);
|
||||
PORTFOLIO_SUMMARY.set({
|
||||
baseCurrencyBalance: data.baseCurrencyBalance,
|
||||
totalCoinValue: data.totalCoinValue,
|
||||
totalValue: data.totalValue,
|
||||
currency: data.currency
|
||||
});
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
51
website/src/routes/api/portfolio/summary/+server.ts
Normal file
51
website/src/routes/api/portfolio/summary/+server.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user, userPortfolio, coin } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const [userData, holdings] = await Promise.all([
|
||||
db.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1),
|
||||
|
||||
db.select({
|
||||
quantity: userPortfolio.quantity,
|
||||
currentPrice: coin.currentPrice
|
||||
})
|
||||
.from(userPortfolio)
|
||||
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||
.where(eq(userPortfolio.userId, userId))
|
||||
]);
|
||||
|
||||
if (!userData[0]) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
let totalCoinValue = 0;
|
||||
|
||||
for (const holding of holdings) {
|
||||
const quantity = Number(holding.quantity);
|
||||
const price = Number(holding.currentPrice);
|
||||
totalCoinValue += quantity * price;
|
||||
}
|
||||
|
||||
const baseCurrencyBalance = Number(userData[0].baseCurrencyBalance);
|
||||
|
||||
return json({
|
||||
baseCurrencyBalance,
|
||||
totalCoinValue,
|
||||
totalValue: baseCurrencyBalance + totalCoinValue,
|
||||
currency: '$'
|
||||
});
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user, userPortfolio, coin } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { user, userPortfolio, coin, transaction } from '$lib/server/db/schema';
|
||||
import { eq, and, sql } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
|
|
@ -24,7 +24,8 @@ export async function GET({ request }) {
|
|||
currentPrice: coin.currentPrice,
|
||||
symbol: coin.symbol,
|
||||
icon: coin.icon,
|
||||
change24h: coin.change24h
|
||||
change24h: coin.change24h,
|
||||
coinId: coin.id
|
||||
})
|
||||
.from(userPortfolio)
|
||||
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||
|
|
@ -37,21 +38,47 @@ export async function GET({ request }) {
|
|||
|
||||
let totalCoinValue = 0;
|
||||
|
||||
const coinHoldings = holdings.map(holding => {
|
||||
const coinHoldings = await Promise.all(holdings.map(async (holding) => {
|
||||
const quantity = Number(holding.quantity);
|
||||
const price = Number(holding.currentPrice);
|
||||
const value = quantity * price;
|
||||
totalCoinValue += value;
|
||||
|
||||
// Calculate average purchase price from buy transactions
|
||||
const avgPriceResult = await db.select({
|
||||
avgPrice: sql<number>`
|
||||
CASE
|
||||
WHEN SUM(${transaction.quantity}) > 0
|
||||
THEN SUM(${transaction.totalBaseCurrencyAmount}) / SUM(${transaction.quantity})
|
||||
ELSE 0
|
||||
END
|
||||
`
|
||||
})
|
||||
.from(transaction)
|
||||
.where(
|
||||
and(
|
||||
eq(transaction.userId, userId),
|
||||
eq(transaction.coinId, holding.coinId),
|
||||
eq(transaction.type, 'BUY')
|
||||
)
|
||||
);
|
||||
|
||||
const avgPurchasePrice = Number(avgPriceResult[0]?.avgPrice || 0);
|
||||
const percentageChange = avgPurchasePrice > 0
|
||||
? ((price - avgPurchasePrice) / avgPurchasePrice) * 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
symbol: holding.symbol,
|
||||
icon: holding.icon,
|
||||
quantity,
|
||||
currentPrice: price,
|
||||
value,
|
||||
change24h: Number(holding.change24h)
|
||||
change24h: Number(holding.change24h),
|
||||
avgPurchasePrice,
|
||||
percentageChange
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
const baseCurrencyBalance = Number(userData[0].baseCurrencyBalance);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
import { toast } from 'svelte-sonner';
|
||||
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { getPublicUrl, getTimeframeInSeconds, timeToLocal } from '$lib/utils.js';
|
||||
import { websocketController, type PriceUpdate, isConnectedStore } from '$lib/stores/websocket';
|
||||
import SEO from '$lib/components/self/SEO.svelte';
|
||||
|
|
@ -124,7 +124,7 @@
|
|||
}
|
||||
}
|
||||
async function handleTradeSuccess() {
|
||||
await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioData()]);
|
||||
await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioSummary()]);
|
||||
}
|
||||
function handlePriceUpdate(priceUpdate: PriceUpdate) {
|
||||
if (coin && priceUpdate.coinSymbol === coinSymbol.toUpperCase()) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import Coinflip from '$lib/components/self/games/Coinflip.svelte';
|
||||
import Slots from '$lib/components/self/games/Slots.svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
function handleBalanceUpdate(newBalance: number) {
|
||||
balance = newBalance;
|
||||
|
||||
if ($PORTFOLIO_DATA) {
|
||||
PORTFOLIO_DATA.update((data) =>
|
||||
if ($PORTFOLIO_SUMMARY) {
|
||||
PORTFOLIO_SUMMARY.update((data) =>
|
||||
data
|
||||
? {
|
||||
...data,
|
||||
|
|
@ -31,8 +31,8 @@
|
|||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($USER_DATA && $PORTFOLIO_DATA) {
|
||||
balance = $PORTFOLIO_DATA.baseCurrencyBalance;
|
||||
if ($USER_DATA && $PORTFOLIO_SUMMARY) {
|
||||
balance = $PORTFOLIO_SUMMARY.baseCurrencyBalance;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
XIcon
|
||||
} from 'lucide-svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount } from 'svelte';
|
||||
import { formatDateWithYear, formatTimeUntil, formatValue, getPublicUrl } from '$lib/utils';
|
||||
|
|
@ -39,12 +39,12 @@
|
|||
let newQuestion = $state('');
|
||||
let creatingQuestion = $state(false);
|
||||
|
||||
let userBalance = $derived($PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance : 0);
|
||||
let userBalance = $derived($PORTFOLIO_SUMMARY ? $PORTFOLIO_SUMMARY.baseCurrencyBalance : 0);
|
||||
|
||||
onMount(() => {
|
||||
fetchQuestions();
|
||||
if ($USER_DATA) {
|
||||
fetchPortfolioData();
|
||||
fetchPortfolioSummary();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
showCreateDialog = false;
|
||||
newQuestion = '';
|
||||
fetchQuestions();
|
||||
fetchPortfolioData();
|
||||
fetchPortfolioSummary();
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to create question', { duration: 20000 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
XIcon
|
||||
} from 'lucide-svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount } from 'svelte';
|
||||
import { formatDateWithYear, getPublicUrl, formatTimeUntil } from '$lib/utils';
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
let placingBet = $state(false);
|
||||
let customBetAmount = $state('');
|
||||
|
||||
let userBalance = $derived($PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance : 0);
|
||||
let userBalance = $derived($PORTFOLIO_SUMMARY ? $PORTFOLIO_SUMMARY.baseCurrencyBalance : 0);
|
||||
let questionId = $derived(parseInt(page.params.id));
|
||||
|
||||
// Chart related
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
onMount(() => {
|
||||
fetchQuestion();
|
||||
if ($USER_DATA) {
|
||||
fetchPortfolioData();
|
||||
fetchPortfolioSummary();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
);
|
||||
customBetAmount = '';
|
||||
fetchQuestion();
|
||||
fetchPortfolioData();
|
||||
fetchPortfolioSummary();
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to place bet');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,29 +10,28 @@
|
|||
import { TrendingUp, DollarSign, Wallet, Receipt, Send } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import SendMoneyModal from '$lib/components/self/SendMoneyModal.svelte';
|
||||
|
||||
// TODO: add type definitions
|
||||
let portfolioData = $state<any>(null);
|
||||
let transactions = $state<any[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let sendMoneyModalOpen = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([fetchPortfolioData(), fetchRecentTransactions()]);
|
||||
await Promise.all([loadPortfolioData(), fetchRecentTransactions()]);
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function fetchPortfolioData() {
|
||||
async function loadPortfolioData() {
|
||||
try {
|
||||
const response = await fetch('/api/portfolio/total');
|
||||
if (response.ok) {
|
||||
portfolioData = await response.json();
|
||||
error = null;
|
||||
} else {
|
||||
const data = await fetchPortfolioData();
|
||||
if (!data) {
|
||||
error = 'Failed to load portfolio data';
|
||||
toast.error('Failed to load portfolio data');
|
||||
} else {
|
||||
error = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch portfolio data:', e);
|
||||
|
|
@ -59,10 +58,11 @@
|
|||
async function retryFetch() {
|
||||
loading = true;
|
||||
error = null;
|
||||
await Promise.all([fetchPortfolioData(), fetchRecentTransactions()]);
|
||||
await Promise.all([loadPortfolioData(), fetchRecentTransactions()]);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
let portfolioData = $derived($PORTFOLIO_DATA);
|
||||
let totalPortfolioValue = $derived(portfolioData ? portfolioData.totalValue : 0);
|
||||
let hasHoldings = $derived(portfolioData && portfolioData.coinHoldings.length > 0);
|
||||
let hasTransactions = $derived(transactions.length > 0);
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
{
|
||||
key: 'coin',
|
||||
label: 'Coin',
|
||||
class: 'w-[30%] min-w-[120px] md:w-[12%]',
|
||||
class: 'w-[25%] min-w-[120px] md:w-[12%]',
|
||||
render: (value: any, row: any) => ({
|
||||
component: 'coin',
|
||||
icon: row.icon,
|
||||
|
|
@ -83,21 +83,32 @@
|
|||
{
|
||||
key: 'quantity',
|
||||
label: 'Quantity',
|
||||
class: 'w-[20%] min-w-[80px] md:w-[12%] font-mono',
|
||||
class: 'w-[15%] min-w-[80px] md:w-[10%] font-mono',
|
||||
sortable: true,
|
||||
render: (value: any) => formatQuantity(value)
|
||||
},
|
||||
{
|
||||
key: 'currentPrice',
|
||||
label: 'Price',
|
||||
class: 'w-[15%] min-w-[70px] md:w-[12%] font-mono',
|
||||
class: 'w-[12%] min-w-[70px] md:w-[10%] font-mono',
|
||||
sortable: true,
|
||||
render: (value: any) => `$${formatPrice(value)}`
|
||||
},
|
||||
{
|
||||
key: 'percentageChange',
|
||||
label: 'P&L %',
|
||||
class: 'w-[15%] min-w-[80px] md:w-[12%]',
|
||||
sortable: true,
|
||||
render: (value: any) => ({
|
||||
component: 'badge',
|
||||
variant: value >= 0 ? 'success' : 'destructive',
|
||||
text: `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'change24h',
|
||||
label: '24h Change',
|
||||
class: 'w-[20%] min-w-[80px] md:w-[12%]',
|
||||
class: 'w-[15%] min-w-[80px] md:w-[12%]',
|
||||
sortable: true,
|
||||
render: (value: any) => ({
|
||||
component: 'badge',
|
||||
|
|
@ -108,7 +119,7 @@
|
|||
{
|
||||
key: 'value',
|
||||
label: 'Value',
|
||||
class: 'w-[15%] min-w-[70px] md:w-[12%] font-mono font-medium',
|
||||
class: 'w-[12%] min-w-[70px] md:w-[10%] font-mono font-medium',
|
||||
sortable: true,
|
||||
defaultSort: true,
|
||||
render: (value: any) => formatValue(value)
|
||||
|
|
@ -222,7 +233,7 @@
|
|||
]);
|
||||
|
||||
async function handleTransferSuccess() {
|
||||
await Promise.all([fetchPortfolioData(), fetchRecentTransactions()]);
|
||||
await Promise.all([loadPortfolioData(), fetchRecentTransactions()]);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -295,11 +306,11 @@
|
|||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-3xl font-bold">
|
||||
{formatValue(portfolioData.baseCurrencyBalance)}
|
||||
{formatValue(portfolioData?.baseCurrencyBalance || 0)}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{totalPortfolioValue > 0
|
||||
? `${((portfolioData.baseCurrencyBalance / totalPortfolioValue) * 100).toFixed(1)}% of portfolio`
|
||||
? `${(((portfolioData?.baseCurrencyBalance || 0) / totalPortfolioValue) * 100).toFixed(1)}% of portfolio`
|
||||
: '100% of portfolio'}
|
||||
</p>
|
||||
</Card.Content>
|
||||
|
|
@ -314,9 +325,9 @@
|
|||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-3xl font-bold">{formatValue(portfolioData.totalCoinValue)}</p>
|
||||
<p class="text-3xl font-bold">{formatValue(portfolioData?.totalCoinValue || 0)}</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{portfolioData.coinHoldings.length} positions
|
||||
{portfolioData?.coinHoldings.length || 0} positions
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
|
|
|||
Reference in a new issue