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