This repository has been archived on 2025-08-19. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
coinstorge/website/src/routes/coin/[coinSymbol]/+page.svelte

734 lines
22 KiB
Svelte
Raw Normal View History

2025-05-21 21:34:22 +03:00
<script lang="ts">
import * as Card from '$lib/components/ui/card';
2025-05-27 16:19:57 +03:00
import * as Select from '$lib/components/ui/select';
2025-05-21 21:34:22 +03:00
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as Avatar from '$lib/components/ui/avatar';
import * as HoverCard from '$lib/components/ui/hover-card';
import TradeModal from '$lib/components/self/TradeModal.svelte';
import CommentSection from '$lib/components/self/CommentSection.svelte';
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
2025-05-27 14:12:29 +03:00
import CoinSkeleton from '$lib/components/self/skeletons/CoinSkeleton.svelte';
2025-06-10 18:42:41 +03:00
import TopHolders from '$lib/components/self/TopHolders.svelte';
import { TrendingUp, TrendingDown, DollarSign, Coins, ChartColumn } from 'lucide-svelte';
2025-06-28 22:59:33 +03:00
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
import { USER_DATA } from '$lib/stores/user-data';
import { fetchPortfolioSummary } from '$lib/stores/portfolio-data';
2025-06-10 16:30:10 +03:00
import { getPublicUrl, getTimeframeInSeconds, timeToLocal } from '$lib/utils.js';
2025-05-27 14:54:19 +03:00
import { websocketController, type PriceUpdate, isConnectedStore } from '$lib/stores/websocket';
2025-05-30 13:15:00 +03:00
import SEO from '$lib/components/self/SEO.svelte';
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
2025-06-28 22:42:05 +03:00
import { browser } from '$app/environment';
2025-05-21 21:34:22 +03:00
const { data } = $props();
let coinSymbol = $derived(data.coinSymbol);
2025-06-28 17:31:05 +03:00
let coin = $state(data.coin);
let loading = $state(false);
let chartData = $state(data.chartData);
let volumeData = $state(data.volumeData);
let userHolding = $state(0);
let buyModalOpen = $state(false);
let sellModalOpen = $state(false);
2025-06-28 17:31:05 +03:00
let selectedTimeframe = $state(data.timeframe || '1m');
2025-05-27 14:54:19 +03:00
let lastPriceUpdateTime = 0;
let shouldSignIn = $state(false);
let previousCoinSymbol = $state<string | null>(null);
2025-06-28 22:42:05 +03:00
// Chart-related variables - only used on client
let chartContainer = $state<HTMLDivElement>();
let chart: any = null;
let candlestickSeries: any = null;
let volumeSeries: any = null;
let chartLibrary: any = null;
2025-06-28 22:59:33 +03:00
let chartInitialized = $state(false);
let resizeHandler: (() => void) | null = null;
2025-06-28 22:42:05 +03:00
2025-05-27 16:19:57 +03:00
const timeframeOptions = [
{ value: '1m', label: '1 minute' },
{ value: '5m', label: '5 minutes' },
{ value: '15m', label: '15 minutes' },
{ value: '1h', label: '1 hour' },
{ value: '4h', label: '4 hours' },
{ value: '1d', label: '1 day' }
];
onMount(async () => {
2025-06-28 22:42:05 +03:00
// Only import chart library on client-side
if (browser) {
try {
2025-06-28 22:59:33 +03:00
const chartModule = await import('lightweight-charts');
chartLibrary = {
createChart: chartModule.createChart,
ColorType: chartModule.ColorType,
CandlestickSeries: chartModule.CandlestickSeries,
HistogramSeries: chartModule.HistogramSeries
};
console.log('Chart library loaded successfully');
2025-06-28 22:42:05 +03:00
} catch (e) {
console.error('Failed to load chart library:', e);
}
}
await loadUserHolding();
2025-06-28 22:59:33 +03:00
if (browser) {
websocketController.setCoin(coinSymbol.toUpperCase());
websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate);
}
previousCoinSymbol = coinSymbol;
2025-05-27 14:54:19 +03:00
});
2025-06-28 22:59:33 +03:00
onDestroy(() => {
if (browser) {
cleanupChart();
if (previousCoinSymbol) {
websocketController.unsubscribeFromPriceUpdates(previousCoinSymbol.toUpperCase());
}
2025-06-28 22:59:33 +03:00
}
});
2025-06-28 22:59:33 +03:00
function cleanupChart() {
if (!browser) return;
if (resizeHandler) {
window.removeEventListener('resize', resizeHandler);
resizeHandler = null;
}
if (chart) {
chart.remove();
chart = null;
}
candlestickSeries = null;
volumeSeries = null;
chartInitialized = false;
}
$effect(() => {
if (coinSymbol && previousCoinSymbol && coinSymbol !== previousCoinSymbol) {
loading = true;
coin = null;
chartData = [];
volumeData = [];
userHolding = 0;
2025-06-28 22:59:33 +03:00
if (browser) {
websocketController.unsubscribeFromPriceUpdates(previousCoinSymbol.toUpperCase());
}
loadCoinData().then(() => {
loadUserHolding();
2025-06-28 22:59:33 +03:00
if (browser) {
websocketController.setCoin(coinSymbol.toUpperCase());
websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate);
}
previousCoinSymbol = coinSymbol;
});
}
});
2025-06-28 22:59:33 +03:00
// Chart initialization effect
$effect(() => {
if (!browser || !chartLibrary || !chartContainer || !chartData || chartData.length === 0) {
return;
}
// Clean up existing chart
cleanupChart();
try {
const { createChart, ColorType, CandlestickSeries, HistogramSeries } = chartLibrary;
chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type: ColorType.Solid, color: 'transparent' },
attributionLogo: false,
panes: {
separatorColor: '#2B2B43',
separatorHoverColor: 'rgba(107, 114, 142, 0.3)',
enableResize: true
}
},
grid: {
vertLines: { color: '#2B2B43' },
horzLines: { color: '#2B2B43' }
},
rightPriceScale: {
borderVisible: false,
scaleMargins: { top: 0.1, bottom: 0.1 },
alignLabels: true,
entireTextOnly: false
},
timeScale: {
borderVisible: false,
timeVisible: true,
barSpacing: 20,
rightOffset: 5,
minBarSpacing: 8
},
crosshair: {
mode: 1,
vertLine: { color: '#758696', width: 1, style: 2, visible: true, labelVisible: true },
horzLine: { color: '#758696', width: 1, style: 2, visible: true, labelVisible: true }
}
});
candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: true,
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
priceFormat: { type: 'price', precision: 8, minMove: 0.00000001 }
});
volumeSeries = chart.addSeries(
HistogramSeries,
{
priceFormat: { type: 'volume' },
priceScaleId: 'volume'
},
1
);
const processedChartData = chartData.map((candle: { open: any; close: any; high: number; low: number; }) => {
if (candle.open === candle.close) {
const basePrice = candle.open;
const variation = basePrice * 0.001;
return {
...candle,
high: Math.max(candle.high, basePrice + variation),
low: Math.min(candle.low, basePrice - variation)
};
}
return candle;
});
candlestickSeries.setData(processedChartData);
volumeSeries.setData(generateVolumeData(chartData, volumeData));
const volumePane = chart.panes()[1];
if (volumePane) volumePane.setHeight(100);
chart.timeScale().fitContent();
// Setup resize handler
resizeHandler = () => {
if (chart && chartContainer) {
chart.applyOptions({ width: chartContainer.clientWidth });
}
};
window.addEventListener('resize', resizeHandler);
resizeHandler(); // Initial call
candlestickSeries.priceScale().applyOptions({ borderColor: '#71649C' });
volumeSeries.priceScale().applyOptions({ borderColor: '#71649C' });
chart.timeScale().applyOptions({ borderColor: '#71649C' });
chartInitialized = true;
console.log('Chart initialized successfully');
} catch (error) {
console.error('Failed to initialize chart:', error);
}
});
async function loadCoinData() {
try {
2025-06-28 17:31:05 +03:00
loading = true;
const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${selectedTimeframe}`);
if (!response.ok) {
toast.error(response.status === 404 ? 'Coin not found' : 'Failed to load coin data');
return;
}
const result = await response.json();
coin = result.coin;
chartData = result.candlestickData || [];
volumeData = result.volumeData || [];
} catch (e) {
console.error('Failed to fetch coin data:', e);
toast.error('Failed to load coin data');
} finally {
loading = false;
}
}
async function loadUserHolding() {
if (!$USER_DATA) return;
2025-05-21 21:34:22 +03:00
try {
const response = await fetch('/api/portfolio/total');
if (response.ok) {
const result = await response.json();
const holding = result.coinHoldings.find((h: any) => h.symbol === coinSymbol.toUpperCase());
userHolding = holding ? holding.quantity : 0;
2025-05-21 21:34:22 +03:00
}
} catch (e) {
console.error('Failed to load user holding:', e);
}
}
2025-06-28 22:59:33 +03:00
async function handleTradeSuccess() {
await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioSummary()]);
}
2025-06-28 22:59:33 +03:00
2025-05-27 14:54:19 +03:00
function handlePriceUpdate(priceUpdate: PriceUpdate) {
2025-06-28 22:59:33 +03:00
if (!browser || !coin || priceUpdate.coinSymbol !== coinSymbol.toUpperCase()) {
return;
}
2025-05-27 14:54:19 +03:00
2025-06-28 22:59:33 +03:00
// throttle updates to prevent excessive UI updates, 1s interval
const now = Date.now();
if (now - lastPriceUpdateTime < 1000) {
return;
2025-05-27 14:54:19 +03:00
}
2025-06-28 22:59:33 +03:00
lastPriceUpdateTime = now;
coin = {
...coin,
currentPrice: priceUpdate.currentPrice,
marketCap: priceUpdate.marketCap,
change24h: priceUpdate.change24h,
volume24h: priceUpdate.volume24h,
...(priceUpdate.poolCoinAmount !== undefined && {
poolCoinAmount: priceUpdate.poolCoinAmount
}),
...(priceUpdate.poolBaseCurrencyAmount !== undefined && {
poolBaseCurrencyAmount: priceUpdate.poolBaseCurrencyAmount
})
};
updateChartRealtime(priceUpdate.currentPrice);
2025-05-27 14:54:19 +03:00
}
function updateChartRealtime(newPrice: number) {
2025-06-28 22:59:33 +03:00
if (!browser || !candlestickSeries || !chart || chartData.length === 0) return;
2025-05-27 14:54:19 +03:00
const timeframeSeconds = getTimeframeInSeconds(selectedTimeframe);
const currentTime = Math.floor(Date.now() / 1000);
const currentCandleTime = Math.floor(currentTime / timeframeSeconds) * timeframeSeconds;
2025-06-10 16:30:10 +03:00
const localCandleTime = timeToLocal(currentCandleTime);
2025-05-27 14:54:19 +03:00
const lastCandle = chartData[chartData.length - 1];
2025-06-10 16:30:10 +03:00
if (lastCandle && lastCandle.time === localCandleTime) {
2025-05-27 14:54:19 +03:00
const updatedCandle = {
2025-06-10 16:30:10 +03:00
time: localCandleTime,
2025-05-27 14:54:19 +03:00
open: lastCandle.open,
high: Math.max(lastCandle.high, newPrice),
low: Math.min(lastCandle.low, newPrice),
close: newPrice
};
candlestickSeries.update(updatedCandle);
chartData[chartData.length - 1] = updatedCandle;
2025-06-10 16:30:10 +03:00
} else if (localCandleTime > (lastCandle?.time || 0)) {
2025-05-27 14:54:19 +03:00
const newCandle = {
2025-06-10 16:30:10 +03:00
time: localCandleTime,
2025-05-27 14:54:19 +03:00
open: newPrice,
high: newPrice,
low: newPrice,
close: newPrice
};
candlestickSeries.update(newCandle);
chartData.push(newCandle);
}
}
async function handleTimeframeChange(timeframe: string) {
selectedTimeframe = timeframe;
loading = true;
2025-06-28 22:59:33 +03:00
// Clean up chart before loading new data
cleanupChart();
await loadCoinData();
loading = false;
}
2025-05-21 21:34:22 +03:00
2025-05-27 16:19:57 +03:00
let currentTimeframeLabel = $derived(
timeframeOptions.find((option) => option.value === selectedTimeframe)?.label || '1 minute'
);
function formatPrice(price: number): string {
if (price < 0.000001) {
return price.toFixed(8);
} else if (price < 0.01) {
return price.toFixed(6);
} else if (price < 1) {
return price.toFixed(4);
} else {
return price.toFixed(2);
}
}
function formatMarketCap(value: number): string {
const num = Number(value);
if (isNaN(num)) return '$0.00';
if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
return `$${num.toFixed(2)}`;
}
function formatSupply(value: number): string {
const num = Number(value);
if (isNaN(num)) return '0';
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
return num.toLocaleString();
}
2025-05-27 16:19:57 +03:00
function generateVolumeData(candlestickData: any[], volumeData: any[]) {
return candlestickData.map((candle, index) => {
// Find corresponding volume data for this time period
const volumePoint = volumeData.find((v) => v.time === candle.time);
const volume = volumePoint ? volumePoint.volume : 0;
return {
time: candle.time,
value: volume,
color: candle.close >= candle.open ? '#26a69a' : '#ef5350'
};
});
}
2025-05-21 21:34:22 +03:00
</script>
2025-05-30 13:15:00 +03:00
<SEO
title={coin
? `${coin.name} (*${coin.symbol}) - Rugplay`
2025-06-28 17:31:05 +03:00
: `Loading ${coinSymbol.toUpperCase()} - Rugplay`}
2025-05-30 13:15:00 +03:00
description={coin
? `Trade ${coin.name} (*${coin.symbol}) in the Rugplay simulation game. Current price: $${formatPrice(coin.currentPrice)}, Market cap: ${formatMarketCap(coin.marketCap)}, 24h change: ${coin.change24h >= 0 ? '+' : ''}${coin.change24h.toFixed(2)}%.`
: `Virtual cryptocurrency trading page for ${coinSymbol.toUpperCase()} in the Rugplay simulation game.`}
keywords={coin
? `${coin.name} cryptocurrency game, *${coin.symbol} virtual trading, ${coin.symbol} price simulation, cryptocurrency trading game, virtual coin ${coin.symbol}`
: `${coinSymbol} virtual cryptocurrency, crypto trading simulation, virtual coin trading`}
2025-06-28 17:31:05 +03:00
image={coin?.icon ? getPublicUrl(coin.icon) : '/apple-touch-icon.png'}
2025-05-30 13:15:00 +03:00
imageAlt={coin ? `${coin.name} (${coin.symbol}) logo` : `${coinSymbol} cryptocurrency logo`}
2025-06-28 17:55:40 +03:00
twitterCard="summary"
2025-05-30 13:15:00 +03:00
/>
<SignInConfirmDialog bind:open={shouldSignIn} />
{#if coin}
<TradeModal bind:open={buyModalOpen} type="BUY" {coin} onSuccess={handleTradeSuccess} />
<TradeModal
bind:open={sellModalOpen}
type="SELL"
{coin}
{userHolding}
onSuccess={handleTradeSuccess}
/>
{/if}
2025-05-27 14:12:29 +03:00
<div class="container mx-auto max-w-7xl p-6">
{#if loading}
<CoinSkeleton />
{:else if !coin}
<div class="flex h-96 items-center justify-center">
<div class="text-center">
<div class="text-muted-foreground mb-4 text-xl">Coin not found</div>
<Button onclick={() => goto('/')}>Go Home</Button>
</div>
</div>
2025-05-27 14:12:29 +03:00
{:else}
<!-- Header Section -->
2025-05-21 21:34:22 +03:00
<header class="mb-8">
2025-05-27 16:19:57 +03:00
<div class="mb-4 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="flex items-center gap-3 sm:gap-4">
<CoinIcon
icon={coin.icon}
symbol={coin.symbol}
name={coin.name}
2025-05-27 16:19:57 +03:00
size={12}
class="border sm:size-16"
/>
2025-05-27 16:19:57 +03:00
<div class="min-w-0 flex-1">
<h1 class="text-2xl font-bold sm:text-4xl">{coin.name}</h1>
<div class="mt-1 flex flex-wrap items-center gap-2">
<Badge variant="outline" class="text-sm sm:text-lg">*{coin.symbol}</Badge>
2025-05-27 14:54:19 +03:00
{#if $isConnectedStore}
2025-05-27 15:06:37 +03:00
<Badge
variant="outline"
class="animate-pulse border-green-500 text-xs text-green-500"
>
2025-05-27 14:54:19 +03:00
● LIVE
</Badge>
{/if}
{#if !coin.isListed}
<Badge variant="destructive">Delisted</Badge>
{/if}
</div>
</div>
</div>
2025-05-27 16:19:57 +03:00
<div class="flex flex-col items-start gap-2 sm:items-end sm:text-right">
2025-05-27 14:54:19 +03:00
<div class="relative">
2025-05-27 16:19:57 +03:00
<p class="text-2xl font-bold sm:text-3xl">
2025-05-27 14:54:19 +03:00
${formatPrice(coin.currentPrice)}
</p>
</div>
2025-05-27 16:19:57 +03:00
<div class="flex items-center gap-2">
{#if coin.change24h >= 0}
<TrendingUp class="h-4 w-4 text-green-500" />
{:else}
<TrendingDown class="h-4 w-4 text-red-500" />
{/if}
2025-05-27 16:19:57 +03:00
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
</Badge>
</div>
</div>
2025-05-21 21:34:22 +03:00
</div>
<!-- Creator Info -->
{#if coin.creatorName}
2025-05-27 16:19:57 +03:00
<div class="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
<span>Created by</span>
<HoverCard.Root>
<HoverCard.Trigger
2025-06-08 21:16:21 +03:00
class="flex min-w-0 max-w-[200px] cursor-pointer items-center gap-1 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8 sm:max-w-[250px]"
onclick={() => goto(`/user/${coin.creatorUsername}`)}
>
2025-06-08 21:16:21 +03:00
<Avatar.Root class="h-4 w-4 flex-shrink-0">
<Avatar.Image src={getPublicUrl(coin.creatorImage)} alt={coin.creatorName} />
<Avatar.Fallback>{coin.creatorName.charAt(0)}</Avatar.Fallback>
</Avatar.Root>
2025-06-08 21:16:21 +03:00
<span class="block truncate font-medium"
>{coin.creatorName} (@{coin.creatorUsername})</span
>
</HoverCard.Trigger>
<HoverCard.Content class="w-80" side="bottom" sideOffset={3}>
<UserProfilePreview userId={coin.creatorId} />
</HoverCard.Content>
</HoverCard.Root>
</div>
{/if}
2025-05-21 21:34:22 +03:00
</header>
<div class="grid gap-6">
<!-- Price Chart with Trading Actions -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Chart (2/3 width) -->
<div class="lg:col-span-2">
2025-06-10 18:42:41 +03:00
<Card.Root class="flex h-full flex-col">
<Card.Header class="pb-4">
<div class="flex items-center justify-between">
<Card.Title class="flex items-center gap-2">
<ChartColumn class="h-5 w-5" />
Price Chart ({selectedTimeframe})
</Card.Title>
2025-05-27 16:19:57 +03:00
<div class="w-24">
<Select.Root
type="single"
bind:value={selectedTimeframe}
onValueChange={handleTimeframeChange}
disabled={loading}
>
<Select.Trigger class="w-full">
{currentTimeframeLabel}
</Select.Trigger>
<Select.Content>
<Select.Group>
{#each timeframeOptions as option}
<Select.Item value={option.value} label={option.label}>
{option.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content class="flex-1 pt-0">
{#if chartData.length === 0}
2025-06-10 18:42:41 +03:00
<div class="flex h-full min-h-[500px] items-center justify-center">
<p class="text-muted-foreground">No trading data available yet</p>
</div>
{:else}
2025-06-10 18:42:41 +03:00
<div class="h-full min-h-[500px] w-full" bind:this={chartContainer}></div>
{/if}
</Card.Content>
</Card.Root>
</div>
2025-06-10 18:42:41 +03:00
<!-- Right side - Trading Actions + Liquidity Pool + Top Holders (1/3 width) -->
<div class="space-y-6 lg:col-span-1">
<!-- Trading Actions -->
<Card.Root>
2025-06-10 18:42:41 +03:00
<Card.Header>
<Card.Title>Trade {coin.symbol}</Card.Title>
{#if userHolding > 0}
<p class="text-muted-foreground text-sm">
You own: {formatSupply(userHolding)}
{coin.symbol}
</p>
{/if}
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content>
{#if $USER_DATA}
<div class="space-y-3">
<Button
class="w-full"
variant="default"
size="lg"
onclick={() => (buyModalOpen = true)}
disabled={!coin.isListed}
>
<TrendingUp class="h-4 w-4" />
Buy {coin.symbol}
</Button>
<Button
class="w-full"
variant="outline"
size="lg"
onclick={() => (sellModalOpen = true)}
disabled={!coin.isListed || userHolding <= 0}
>
<TrendingDown class="h-4 w-4" />
Sell {coin.symbol}
</Button>
</div>
{:else}
<div class="py-4 text-center">
<p class="text-muted-foreground mb-3 text-sm">Sign in to start trading</p>
<Button onclick={() => (shouldSignIn = true)}>Sign In</Button>
</div>
{/if}
</Card.Content>
</Card.Root>
<!-- Liquidity Pool -->
<Card.Root>
2025-06-10 18:42:41 +03:00
<Card.Content>
<div class="space-y-4">
<div>
<h4 class="mb-3 font-medium">Pool Composition</h4>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">{coin.symbol}:</span>
2025-05-27 14:54:19 +03:00
<span class="font-mono text-sm">
{formatSupply(coin.poolCoinAmount)}
</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Base Currency:</span>
<span class="font-mono text-sm">
${Number(coin.poolBaseCurrencyAmount).toLocaleString()}
</span>
</div>
</div>
</div>
<div>
<h4 class="mb-3 font-medium">Pool Stats</h4>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Total Liquidity:</span>
<span class="font-mono text-sm">
${(Number(coin.poolBaseCurrencyAmount) * 2).toLocaleString()}
</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Current Price:</span>
<span class="font-mono text-sm">${formatPrice(coin.currentPrice)}</span>
</div>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
2025-06-10 18:42:41 +03:00
<!-- Top Holders -->
<TopHolders coinSymbol={coin.symbol} />
</div>
</div>
<!-- Statistics Grid -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Market Cap -->
2025-05-27 15:06:37 +03:00
<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" />
Market Cap
</Card.Title>
2025-05-21 21:34:22 +03:00
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content>
2025-05-27 14:54:19 +03:00
<p class="text-xl font-bold">
{formatMarketCap(coin.marketCap)}
</p>
2025-05-21 21:34:22 +03:00
</Card.Content>
</Card.Root>
<!-- 24h Volume -->
2025-05-27 15:06:37 +03:00
<Card.Root class="gap-1">
<Card.Header>
<Card.Title class="flex items-center gap-2 text-sm font-medium">
<ChartColumn class="h-4 w-4" />
24h Volume
</Card.Title>
2025-05-21 21:34:22 +03:00
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content>
2025-05-27 14:54:19 +03:00
<p class="text-xl font-bold">
{formatMarketCap(coin.volume24h)}
</p>
2025-05-21 21:34:22 +03:00
</Card.Content>
</Card.Root>
<!-- Circulating Supply -->
2025-05-27 15:06:37 +03:00
<Card.Root class="gap-1">
<Card.Header>
<Card.Title class="flex items-center gap-2 text-sm font-medium">
<Coins class="h-4 w-4" />
Circulating Supply
</Card.Title>
2025-05-21 21:34:22 +03:00
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content>
2025-05-27 15:06:37 +03:00
<p class="text-xl font-bold">
2025-05-27 16:19:57 +03:00
{formatSupply(coin.circulatingSupply)}<span
class="text-muted-foreground ml-1 text-xs"
>
2025-05-27 15:06:37 +03:00
of {formatSupply(coin.initialSupply)} total
</span>
2025-05-21 21:34:22 +03:00
</p>
</Card.Content>
</Card.Root>
<!-- 24h Change -->
2025-05-27 15:06:37 +03:00
<Card.Root class="gap-1">
<Card.Header>
<Card.Title class="text-sm font-medium">24h Change</Card.Title>
</Card.Header>
2025-06-10 18:42:41 +03:00
<Card.Content>
<div class="flex items-center gap-2">
{#if coin.change24h >= 0}
<TrendingUp class="h-4 w-4 text-green-500" />
{:else}
<TrendingDown class="h-4 w-4 text-red-500" />
{/if}
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
{coin.change24h >= 0 ? '+' : ''}{Number(coin.change24h).toFixed(2)}%
</Badge>
</div>
</Card.Content>
</Card.Root>
2025-05-21 21:34:22 +03:00
</div>
<!-- Comments Section -->
<CommentSection {coinSymbol} />
2025-05-21 21:34:22 +03:00
</div>
2025-05-27 14:12:29 +03:00
{/if}
</div>