2025-05-21 21:34:22 +03:00
< script lang = "ts" >
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
2025-05-23 16:26:02 +03:00
import { Button } from '$lib/components/ui/button';
import * as Avatar from '$lib/components/ui/avatar';
import * as HoverCard from '$lib/components/ui/hover-card';
2025-05-23 19:48:23 +03:00
import TradeModal from '$lib/components/self/TradeModal.svelte';
2025-05-24 19:28:38 +03:00
import CommentSection from '$lib/components/self/CommentSection.svelte';
2025-05-25 18:44:06 +03:00
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
2025-05-26 13:05:47 +03:00
import { TrendingUp , TrendingDown , DollarSign , Coins , ChartColumn } from 'lucide-svelte';
2025-05-23 16:26:02 +03:00
import {
createChart,
ColorType,
type IChartApi,
2025-05-23 19:48:23 +03:00
CandlestickSeries,
HistogramSeries
2025-05-23 16:26:02 +03:00
} from 'lightweight-charts';
2025-05-21 21:34:22 +03:00
import { onMount } from 'svelte';
2025-05-23 16:26:02 +03:00
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
2025-05-23 19:48:23 +03:00
import CoinIcon from '$lib/components/self/CoinIcon.svelte';
import { USER_DATA } from '$lib/stores/user-data';
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
2025-05-25 18:44:06 +03:00
import { getPublicUrl } from '$lib/utils.js';
2025-05-21 21:34:22 +03:00
2025-05-23 16:26:02 +03:00
const { data } = $props();
const coinSymbol = data.coinSymbol;
let coin = $state< any > (null);
let loading = $state(true);
let chartData = $state< any [ ] > ([]);
2025-05-23 19:48:23 +03:00
let volumeData = $state< any [ ] > ([]);
let userHolding = $state(0);
let buyModalOpen = $state(false);
let sellModalOpen = $state(false);
let selectedTimeframe = $state('1m');
2025-05-23 16:26:02 +03:00
onMount(async () => {
2025-05-23 19:48:23 +03:00
await loadCoinData();
await loadUserHolding();
});
async function loadCoinData() {
2025-05-23 16:26:02 +03:00
try {
2025-05-23 19:48:23 +03:00
const response = await fetch(`/api/coin/${ coinSymbol } ?timeframe=${ selectedTimeframe } `);
2025-05-23 16:26:02 +03:00
if (!response.ok) {
2025-05-23 19:48:23 +03:00
toast.error(response.status === 404 ? 'Coin not found' : 'Failed to load coin data');
2025-05-23 16:26:02 +03:00
return;
}
const result = await response.json();
coin = result.coin;
2025-05-23 19:48:23 +03:00
chartData = result.candlestickData || [];
volumeData = result.volumeData || [];
2025-05-23 16:26:02 +03:00
} catch (e) {
console.error('Failed to fetch coin data:', e);
toast.error('Failed to load coin data');
} finally {
loading = false;
}
2025-05-23 19:48:23 +03:00
}
async function loadUserHolding() {
if (!$USER_DATA) return;
2025-05-21 21:34:22 +03:00
2025-05-23 19:48:23 +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
}
2025-05-23 19:48:23 +03:00
} catch (e) {
console.error('Failed to load user holding:', e);
}
}
async function handleTradeSuccess() {
await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioData()]);
}
async function handleTimeframeChange(timeframe: string) {
selectedTimeframe = timeframe;
loading = true;
if (chart) {
chart.remove();
chart = null;
}
await loadCoinData();
loading = false;
}
2025-05-21 21:34:22 +03:00
2025-05-23 19:48:23 +03:00
function generateVolumeData(candlestickData: any[], volumeData: any[]) {
return candlestickData.map((candle, index) => {
// Find corresponding volume data for this time period
2025-05-23 21:45:41 +03:00
const volumePoint = volumeData.find((v) => v.time === candle.time);
2025-05-23 19:48:23 +03:00
const volume = volumePoint ? volumePoint.volume : 0;
return {
time: candle.time,
value: volume,
color: candle.close >= candle.open ? '#26a69a' : '#ef5350'
};
});
2025-05-23 16:26:02 +03:00
}
2025-05-21 21:34:22 +03:00
2025-05-23 16:26:02 +03:00
let chartContainer = $state< HTMLDivElement > ();
let chart: IChartApi | null = null;
2025-05-21 21:34:22 +03:00
2025-05-23 16:26:02 +03:00
$effect(() => {
2025-05-23 19:48:23 +03:00
if (chart && chartData.length > 0) {
chart.remove();
chart = null;
}
if (chartContainer && chartData.length > 0) {
2025-05-23 16:26:02 +03:00
chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type : ColorType . Solid , color : 'transparent' } ,
2025-05-23 19:48:23 +03:00
attributionLogo: false,
panes: {
separatorColor: '#2B2B43',
separatorHoverColor: 'rgba(107, 114, 142, 0.3)',
enableResize: true
}
2025-05-23 16:26:02 +03:00
},
grid: {
vertLines: { color : '#2B2B43' } ,
horzLines: { color : '#2B2B43' }
},
rightPriceScale: {
2025-05-23 19:48:23 +03:00
borderVisible: false,
scaleMargins: { top : 0.1 , bottom : 0.1 } ,
alignLabels: true,
entireTextOnly: false
2025-05-23 16:26:02 +03:00
},
timeScale: {
borderVisible: false,
2025-05-23 19:48:23 +03:00
timeVisible: true,
barSpacing: 20,
rightOffset: 5,
minBarSpacing: 8
2025-05-23 16:26:02 +03:00
},
crosshair: {
2025-05-23 19:48:23 +03:00
mode: 1,
vertLine: { color : '#758696' , width : 1 , style : 2 , visible : true , labelVisible : true } ,
horzLine: { color : '#758696' , width : 1 , style : 2 , visible : true , labelVisible : true }
2025-05-23 16:26:02 +03:00
}
2025-05-21 21:34:22 +03:00
});
2025-05-23 16:26:02 +03:00
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
2025-05-23 19:48:23 +03:00
borderVisible: true,
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
2025-05-23 16:26:02 +03:00
wickUpColor: '#26a69a',
2025-05-23 19:48:23 +03:00
wickDownColor: '#ef5350',
priceFormat: { type : 'price' , precision : 8 , minMove : 0.00000001 }
2025-05-23 16:26:02 +03:00
});
2025-05-23 21:45:41 +03:00
const volumeSeries = chart.addSeries(
HistogramSeries,
{
priceFormat: { type : 'volume' } ,
priceScaleId: 'volume'
},
1
);
2025-05-23 16:26:02 +03:00
2025-05-23 19:48:23 +03:00
const processedChartData = chartData.map((candle) => {
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);
2025-05-21 21:34:22 +03:00
2025-05-23 19:48:23 +03:00
chart.timeScale().fitContent();
const handleResize = () => chart?.applyOptions({ width : chartContainer?.clientWidth } );
2025-05-23 16:26:02 +03:00
window.addEventListener('resize', handleResize);
handleResize();
2025-05-23 19:48:23 +03:00
candlestickSeries.priceScale().applyOptions({ borderColor : '#71649C' } );
volumeSeries.priceScale().applyOptions({ borderColor : '#71649C' } );
chart.timeScale().applyOptions({ borderColor : '#71649C' } );
2025-05-23 16:26:02 +03:00
return () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.remove();
chart = null;
}
};
}
2025-05-21 21:34:22 +03:00
});
2025-05-23 16:26:02 +03:00
function formatPrice(price: number): string {
2025-05-23 21:45:41 +03:00
if (price < 0.000001 ) {
return price.toFixed(8);
} else if (price < 0.01 ) {
2025-05-23 16:26:02 +03:00
return price.toFixed(6);
} else if (price < 1 ) {
return price.toFixed(4);
} else {
return price.toFixed(2);
}
}
function formatMarketCap(value: number): string {
2025-05-23 21:45:41 +03:00
const num = Number(value);
if (isNaN(num)) return '$0.00';
if (num >= 1e9) return `$${( num / 1 e9 ). toFixed ( 2 )} B`;
if (num >= 1e6) return `$${( num / 1 e6 ). toFixed ( 2 )} M`;
if (num >= 1e3) return `$${( num / 1 e3 ). toFixed ( 2 )} K`;
return `$${ num . toFixed ( 2 )} `;
2025-05-23 16:26:02 +03:00
}
function formatSupply(value: number): string {
2025-05-23 21:45:41 +03:00
const num = Number(value);
if (isNaN(num)) return '0';
if (num >= 1e9) return `${( num / 1 e9 ). toFixed ( 2 )} B`;
if (num >= 1e6) return `${( num / 1 e6 ). toFixed ( 2 )} M`;
if (num >= 1e3) return `${( num / 1 e3 ). toFixed ( 2 )} K`;
return num.toLocaleString();
2025-05-23 16:26:02 +03:00
}
2025-05-21 21:34:22 +03:00
< / script >
2025-05-23 16:26:02 +03:00
< svelte:head >
< title > { coin ? `$ { coin . name } ( $ { coin . symbol }) ` : 'Loading...' } - Rugplay</ title >
< / svelte:head >
2025-05-23 19:48:23 +03:00
{ #if coin }
< TradeModal bind:open = { buyModalOpen } type="BUY" { coin } onSuccess = { handleTradeSuccess } / >
< TradeModal
bind:open={ sellModalOpen }
type="SELL"
{ coin }
{ userHolding }
onSuccess={ handleTradeSuccess }
/>
{ /if }
2025-05-23 16:26:02 +03:00
{ #if loading }
< div class = "container mx-auto max-w-7xl p-6" >
< div class = "flex h-96 items-center justify-center" >
< div class = "text-center" >
< div class = "mb-4 text-xl" > Loading coin data...< / div >
< / div >
< / div >
< / div >
{ :else if ! coin }
< div class = "container mx-auto max-w-7xl p-6" >
< 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 >
< / div >
{ : else }
< div class = "container mx-auto max-w-7xl p-6" >
<!-- Header Section -->
2025-05-21 21:34:22 +03:00
< header class = "mb-8" >
2025-05-23 16:26:02 +03:00
< div class = "mb-4 flex items-start justify-between" >
< div class = "flex items-center gap-4" >
2025-05-23 19:48:23 +03:00
< CoinIcon
icon={ coin . icon }
symbol={ coin . symbol }
name={ coin . name }
size={ 16 }
class="border"
/>
2025-05-23 16:26:02 +03:00
< div >
< h1 class = "text-4xl font-bold" > { coin . name } </ h1 >
< div class = "mt-1 flex items-center gap-2" >
< Badge variant = "outline" class = "text-lg" > *{ coin . symbol } </ Badge >
{ #if ! coin . isListed }
< Badge variant = "destructive" > Delisted< / Badge >
{ /if }
< / div >
< / div >
< / div >
< div class = "text-right" >
< p class = "text-3xl font-bold" >
${ formatPrice ( coin . currentPrice )}
< / p >
< div class = "mt-2 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' } >
2025-05-23 21:45:41 +03:00
{ coin . change24h >= 0 ? '+' : '' }{ Number ( coin . change24h ). toFixed ( 2 )} %
2025-05-23 16:26:02 +03:00
< / Badge >
< / div >
< / div >
2025-05-21 21:34:22 +03:00
< / div >
2025-05-23 16:26:02 +03:00
<!-- Creator Info -->
{ #if coin . creatorName }
< div class = "text-muted-foreground flex items-center gap-2 text-sm" >
< span > Created by< / span >
< HoverCard.Root >
< HoverCard.Trigger
2025-05-23 21:45:41 +03:00
class="flex cursor-pointer items-center gap-1 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
2025-05-25 18:44:06 +03:00
onclick={() => goto ( `/user/$ { coin . creatorUsername } `) }
2025-05-23 16:26:02 +03:00
>
< Avatar.Root class = "h-4 w-4" >
2025-05-25 18:44:06 +03:00
< Avatar.Image src = { getPublicUrl ( coin . creatorImage )} alt= { coin . creatorName } />
2025-05-23 16:26:02 +03:00
< Avatar.Fallback > { coin . creatorName . charAt ( 0 )} </ Avatar.Fallback >
< / Avatar.Root >
< span class = "font-medium" > { coin . creatorName } (@{ coin . creatorUsername } )</ span >
< / HoverCard.Trigger >
< HoverCard.Content class = "w-80" side = "bottom" sideOffset = { 3 } >
2025-05-25 18:44:06 +03:00
< UserProfilePreview userId = { coin . creatorId } / >
2025-05-23 16:26:02 +03:00
< / HoverCard.Content >
< / HoverCard.Root >
< / div >
{ /if }
2025-05-21 21:34:22 +03:00
< / header >
< div class = "grid gap-6" >
2025-05-23 16:26:02 +03:00
<!-- 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" >
< Card.Root >
< Card.Header class = "pb-4" >
2025-05-23 19:48:23 +03:00
< 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 >
< div class = "flex gap-1" >
{ #each [ '1m' , '5m' , '15m' , '1h' , '4h' , '1d' ] as timeframe }
< Button
variant={ selectedTimeframe === timeframe ? 'default' : 'outline' }
size="sm"
onclick={() => handleTimeframeChange ( timeframe )}
disabled={ loading }
>
{ timeframe }
< / Button >
{ /each }
< / div >
< / div >
2025-05-23 16:26:02 +03:00
< / Card.Header >
< Card.Content class = "pt-0" >
2025-05-23 19:48:23 +03:00
{ #if chartData . length === 0 }
< div class = "flex h-[500px] items-center justify-center" >
< p class = "text-muted-foreground" > No trading data available yet< / p >
< / div >
{ : else }
< div class = "h-[500px] w-full" bind:this = { chartContainer } > </div >
{ /if }
2025-05-23 16:26:02 +03:00
< / Card.Content >
< / Card.Root >
< / div >
<!-- Right side - Trading Actions + Liquidity Pool (1/3 width) -->
< div class = "space-y-6 lg:col-span-1" >
<!-- Trading Actions -->
< Card.Root >
< Card.Header class = "pb-4" >
< Card.Title > Trade { coin . symbol } </ Card.Title >
2025-05-23 19:48:23 +03:00
{ #if userHolding > 0 }
< p class = "text-muted-foreground text-sm" >
2025-05-23 21:45:41 +03:00
You own: { formatSupply ( userHolding )}
2025-05-23 19:48:23 +03:00
{ coin . symbol }
< / p >
{ /if }
2025-05-23 16:26:02 +03:00
< / Card.Header >
< Card.Content class = "pt-0" >
2025-05-23 19:48:23 +03:00
{ #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 = {() => goto ( '/' )} > Sign In </ Button >
< / div >
{ /if }
2025-05-23 16:26:02 +03:00
< / Card.Content >
< / Card.Root >
<!-- Liquidity Pool -->
< Card.Root >
< Card.Header class = "pb-4" >
< Card.Title > Liquidity Pool< / Card.Title >
< / Card.Header >
< Card.Content class = "pt-0" >
< 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 >
< 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 >
2025-05-23 21:45:41 +03:00
< span class = "font-mono text-sm" >
${ Number ( coin . poolBaseCurrencyAmount ). toLocaleString ()}
< / span >
2025-05-23 16:26:02 +03:00
< / 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 >
2025-05-23 21:45:41 +03:00
< span class = "font-mono text-sm" >
${( Number ( coin . poolBaseCurrencyAmount ) * 2 ). toLocaleString ()}
< / span >
2025-05-23 16:26:02 +03:00
< / div >
< div class = "flex justify-between" >
2025-05-23 21:45:41 +03:00
< span class = "text-muted-foreground text-sm" > Current Price:< / span >
< span class = "font-mono text-sm" > ${ formatPrice ( coin . currentPrice )} </ span >
2025-05-23 16:26:02 +03:00
< / div >
< / div >
< / div >
< / div >
< / Card.Content >
< / Card.Root >
< / div >
< / div >
<!-- Statistics Grid -->
< div class = "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4" >
<!-- Market Cap -->
2025-05-21 21:34:22 +03:00
< Card.Root >
2025-05-23 16:26:02 +03:00
< Card.Header class = "pb-2" >
< 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-05-23 16:26:02 +03:00
< Card.Content class = "pt-0" >
< p class = "text-xl font-bold" > { formatMarketCap ( coin . marketCap )} </ p >
2025-05-21 21:34:22 +03:00
< / Card.Content >
< / Card.Root >
2025-05-23 16:26:02 +03:00
<!-- 24h Volume -->
2025-05-21 21:34:22 +03:00
< Card.Root >
2025-05-23 16:26:02 +03:00
< Card.Header class = "pb-2" >
< 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-05-23 16:26:02 +03:00
< Card.Content class = "pt-0" >
< p class = "text-xl font-bold" > { formatMarketCap ( coin . volume24h )} </ p >
2025-05-21 21:34:22 +03:00
< / Card.Content >
< / Card.Root >
2025-05-23 16:26:02 +03:00
<!-- Circulating Supply -->
2025-05-21 21:34:22 +03:00
< Card.Root >
2025-05-23 16:26:02 +03:00
< Card.Header class = "pb-2" >
< 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-05-23 16:26:02 +03:00
< Card.Content class = "pt-0" >
< p class = "text-xl font-bold" > { formatSupply ( coin . circulatingSupply )} </ p >
< p class = "text-muted-foreground text-xs" >
of { formatSupply ( coin . initialSupply )} total
2025-05-21 21:34:22 +03:00
< / p >
< / Card.Content >
< / Card.Root >
2025-05-23 16:26:02 +03:00
<!-- 24h Change -->
< Card.Root >
< Card.Header class = "pb-2" >
< Card.Title class = "text-sm font-medium" > 24h Change< / Card.Title >
< / Card.Header >
< Card.Content class = "pt-0" >
< 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" >
2025-05-23 21:45:41 +03:00
{ coin . change24h >= 0 ? '+' : '' }{ Number ( coin . change24h ). toFixed ( 2 )} %
2025-05-23 16:26:02 +03:00
< / Badge >
< / div >
< / Card.Content >
< / Card.Root >
2025-05-21 21:34:22 +03:00
< / div >
2025-05-24 19:28:38 +03:00
<!-- Comments Section -->
< CommentSection { coinSymbol } />
2025-05-21 21:34:22 +03:00
< / div >
2025-05-23 16:26:02 +03:00
< / div >
{ /if }