feat: add username availability check API endpoint

feat: create user image retrieval API endpoint

feat: enhance coin page with dynamic data fetching and improved UI

feat: implement coin creation form with validation and submission logic

feat: add user settings page with profile update functionality
This commit is contained in:
Face 2025-05-23 16:26:02 +03:00
parent 9aa4ba157b
commit 16ad425bb5
48 changed files with 3030 additions and 326 deletions

View file

@ -1,144 +1,452 @@
<script lang="ts">
import { page } from '$app/stores';
import { coins } from '$lib/data/coins';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { createChart, CandlestickSeries, type Time, ColorType } from 'lightweight-charts';
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 {
TrendingUp,
TrendingDown,
DollarSign,
Coins,
ChartColumn,
CalendarDays
} from 'lucide-svelte';
import {
createChart,
ColorType,
type Time,
type IChartApi,
CandlestickSeries
} from 'lightweight-charts';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { getPublicUrl } from '$lib/utils';
import { toast } from 'svelte-sonner';
const coin = coins.find((c) => c.symbol === $page.params.coinSymbol);
const { data } = $props();
const coinSymbol = data.coinSymbol;
// Generate mock candlestick data
const candleData = Array.from({ length: 30 }, (_, i) => {
const basePrice = coin?.price || 100;
const date = new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000);
const open = basePrice * (1 + Math.sin(i / 5) * 0.1);
const close = basePrice * (1 + Math.sin((i + 1) / 5) * 0.1);
const high = Math.max(open, close) * (1 + Math.random() * 0.02);
const low = Math.min(open, close) * (1 - Math.random() * 0.02);
let coin = $state<any>(null);
let priceHistory = $state<any[]>([]);
let loading = $state(true);
let creatorImageUrl = $state<string | null>(null);
let chartData = $state<any[]>([]);
return {
time: Math.floor(date.getTime() / 1000) as Time,
open,
high,
low,
close
};
onMount(async () => {
try {
const response = await fetch(`/api/coin/${coinSymbol}`);
if (!response.ok) {
if (response.status === 404) {
toast.error('Coin not found');
} else {
toast.error('Failed to load coin data');
}
return;
}
const result = await response.json();
coin = result.coin;
priceHistory = result.priceHistory;
chartData = generateCandlesticksFromHistory(priceHistory);
if (coin.creatorId) {
try {
const imageResponse = await fetch(`/api/user/${coin.creatorId}/image`);
const imageResult = await imageResponse.json();
creatorImageUrl = imageResult.url;
} catch (e) {
console.error('Failed to load creator image:', e);
}
}
} catch (e) {
console.error('Failed to fetch coin data:', e);
toast.error('Failed to load coin data');
} finally {
loading = false;
}
});
let chartContainer: HTMLDivElement;
function generateCandlesticksFromHistory(history: any[]) {
const dailyData = new Map();
onMount(() => {
const chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type: ColorType.Solid, color: 'transparent' },
attributionLogo: false
},
grid: {
vertLines: { color: '#2B2B43' },
horzLines: { color: '#2B2B43' }
},
rightPriceScale: {
borderVisible: false
},
timeScale: {
borderVisible: false,
timeVisible: true
},
crosshair: {
mode: 1
history.forEach((p) => {
const date = new Date(p.timestamp);
const dayKey = Math.floor(date.getTime() / (24 * 60 * 60 * 1000));
if (!dailyData.has(dayKey)) {
dailyData.set(dayKey, {
time: dayKey * 24 * 60 * 60,
open: p.price,
high: p.price,
low: p.price,
close: p.price,
prices: [p.price]
});
} else {
const dayData = dailyData.get(dayKey);
dayData.high = Math.max(dayData.high, p.price);
dayData.low = Math.min(dayData.low, p.price);
dayData.close = p.price;
dayData.prices.push(p.price);
}
});
const candlesticks = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350'
});
return Array.from(dailyData.values())
.map((d) => ({
time: d.time as Time,
open: d.open,
high: d.high,
low: d.low,
close: d.close
}))
.sort((a, b) => (a.time as number) - (b.time as number));
}
candlesticks.setData(candleData);
chart.timeScale().fitContent();
let chartContainer = $state<HTMLDivElement>();
let chart: IChartApi | null = null;
const handleResize = () => {
chart.applyOptions({
width: chartContainer.clientWidth
$effect(() => {
if (chartContainer && chartData.length > 0 && !chart) {
chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type: ColorType.Solid, color: 'transparent' },
attributionLogo: false
},
grid: {
vertLines: { color: '#2B2B43' },
horzLines: { color: '#2B2B43' }
},
rightPriceScale: {
borderVisible: false
},
timeScale: {
borderVisible: false,
timeVisible: true
},
crosshair: {
mode: 1
}
});
};
window.addEventListener('resize', handleResize);
handleResize();
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350'
});
return () => {
window.removeEventListener('resize', handleResize);
chart.remove();
};
candlestickSeries.setData(chartData);
chart.timeScale().fitContent();
const handleResize = () => {
chart?.applyOptions({
width: chartContainer?.clientWidth
});
};
window.addEventListener('resize', handleResize);
handleResize();
return () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.remove();
chart = null;
}
};
}
});
function formatPrice(price: number): string {
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 {
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
return `$${value.toFixed(2)}`;
}
function formatSupply(value: number): string {
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3) return `${(value / 1e3).toFixed(2)}K`;
return value.toLocaleString();
}
</script>
<div class="container mx-auto p-6">
{#if coin}
<header class="mb-8">
<div class="flex items-center justify-between">
<h1 class="text-4xl font-bold">{coin.name} ({coin.symbol})</h1>
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
<svelte:head>
<title>{coin ? `${coin.name} (${coin.symbol})` : 'Loading...'} - Rugplay</title>
</svelte:head>
{#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>
<p class="mt-4 text-3xl font-semibold">
${coin.price.toLocaleString(undefined, {
minimumFractionDigits: coin.price < 1 ? 3 : 2,
maximumFractionDigits: coin.price < 1 ? 3 : 2
})}
</p>
</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 -->
<header class="mb-8">
<div class="mb-4 flex items-start justify-between">
<div class="flex items-center gap-4">
<div
class="bg-muted/50 flex h-16 w-16 items-center justify-center overflow-hidden rounded-full border"
>
{#if coin.icon}
<img
src={getPublicUrl(coin.icon)}
alt={coin.name}
class="h-full w-full object-cover"
/>
{:else}
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-600 text-xl font-bold text-white"
>
{coin.symbol.slice(0, 2)}
</div>
{/if}
</div>
<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'}>
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
</Badge>
</div>
</div>
</div>
<!-- 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
class="flex cursor-pointer items-center gap-2 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
onclick={() => goto(`/user/${coin.creatorId}`)}
>
<Avatar.Root class="h-4 w-4">
<Avatar.Image src={creatorImageUrl} alt={coin.creatorName} />
<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}>
<div class="flex justify-between space-x-4">
<Avatar.Root class="h-14 w-14">
<Avatar.Image src={creatorImageUrl} alt={coin.creatorName} />
<Avatar.Fallback>{coin.creatorName.charAt(0)}</Avatar.Fallback>
</Avatar.Root>
<div class="flex-1 space-y-1">
<h4 class="text-sm font-semibold">{coin.creatorName}</h4>
<p class="text-muted-foreground text-sm">@{coin.creatorUsername}</p>
{#if coin.creatorBio}
<p class="text-sm">{coin.creatorBio}</p>
{/if}
<div class="flex items-center pt-2">
<CalendarDays class="mr-2 h-4 w-4 opacity-70" />
<span class="text-muted-foreground text-xs">
Joined {new Date(coin.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long'
})}
</span>
</div>
</div>
</div>
</HoverCard.Content>
</HoverCard.Root>
</div>
{/if}
</header>
<div class="grid gap-6">
<Card.Root>
<Card.Header>
<Card.Title>Price Chart</Card.Title>
</Card.Header>
<Card.Content>
<div class="h-[400px] w-full" bind:this={chartContainer}></div>
</Card.Content>
</Card.Root>
<!-- 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">
<Card.Title class="flex items-center gap-2">
<ChartColumn class="h-5 w-5" />
Price Chart
</Card.Title>
</Card.Header>
<Card.Content class="pt-0">
<div class="h-[400px] w-full" bind:this={chartContainer}></div>
</Card.Content>
</Card.Root>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<!-- 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>
</Card.Header>
<Card.Content class="pt-0">
<div class="space-y-3">
<Button class="w-full" variant="default" size="lg">
<TrendingUp class="mr-2 h-4 w-4" />
Buy {coin.symbol}
</Button>
<Button class="w-full" variant="outline" size="lg">
<TrendingDown class="mr-2 h-4 w-4" />
Sell {coin.symbol}
</Button>
</div>
</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>
<span class="font-mono text-sm"
>${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"
>${(coin.poolBaseCurrencyAmount * 2).toLocaleString()}</span
>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Price Impact:</span>
<Badge variant="success" class="text-xs">Low</Badge>
</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 -->
<Card.Root>
<Card.Header>
<Card.Title>Market Cap</Card.Title>
<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>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">${(coin.marketCap / 1000000000).toFixed(2)}B</p>
<Card.Content class="pt-0">
<p class="text-xl font-bold">{formatMarketCap(coin.marketCap)}</p>
</Card.Content>
</Card.Root>
<!-- 24h Volume -->
<Card.Root>
<Card.Header>
<Card.Title>24h Volume</Card.Title>
<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>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">${(coin.volume24h / 1000000000).toFixed(2)}B</p>
<Card.Content class="pt-0">
<p class="text-xl font-bold">{formatMarketCap(coin.volume24h)}</p>
</Card.Content>
</Card.Root>
<!-- Circulating Supply -->
<Card.Root>
<Card.Header>
<Card.Title>24h Change</Card.Title>
<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>
</Card.Header>
<Card.Content>
<p class="text-2xl font-semibold">
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
</Badge>
<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
</p>
</Card.Content>
</Card.Root>
<!-- 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">
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
</Badge>
</div>
</Card.Content>
</Card.Root>
</div>
</div>
{:else}
<p>Coin not found</p>
{/if}
</div>
</div>
{/if}

View file

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
coinSymbol: params.coinSymbol
};
}

View file

@ -0,0 +1,329 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Alert, AlertDescription } from '$lib/components/ui/alert';
import { Separator } from '$lib/components/ui/separator';
import { Info, Loader2, Coins, ImagePlus } from 'lucide-svelte';
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
import { onMount } from 'svelte';
import { CREATION_FEE, INITIAL_LIQUIDITY, TOTAL_COST } from '$lib/data/constants';
import { toast } from 'svelte-sonner';
let name = $state('');
let symbol = $state('');
let iconFile = $state<File | null>(null);
let iconPreview = $state<string | null>(null);
let isSubmitting = $state(false);
let error = $state('');
onMount(() => {
fetchPortfolioData();
});
let nameError = $derived(
name.length > 0 && (name.length < 2 || name.length > 255)
? 'Name must be between 2 and 255 characters'
: ''
);
let symbolError = $derived(
symbol.length > 0 && (symbol.length < 2 || symbol.length > 10)
? 'Symbol must be between 2 and 10 characters'
: ''
);
let iconError = $derived(
iconFile && iconFile.size > 1 * 1024 * 1024 ? 'Icon must be smaller than 1MB' : ''
);
let isFormValid = $derived(
name.length >= 2 && symbol.length >= 2 && !nameError && !symbolError && !iconError
);
let hasEnoughFunds = $derived(
$PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance >= TOTAL_COST : false
);
let canSubmit = $derived(isFormValid && hasEnoughFunds && !isSubmitting);
function handleIconChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
if (file.type.startsWith('image/')) {
iconFile = file;
console.log(iconFile.size);
const reader = new FileReader();
reader.onload = (e) => {
iconPreview = e.target?.result as string;
};
reader.readAsDataURL(file);
} else {
error = 'Please select a valid image file';
target.value = '';
}
} else {
iconFile = null;
iconPreview = null;
}
}
async function handleSubmit(event: { preventDefault: () => void }) {
event.preventDefault();
if (!canSubmit) return;
isSubmitting = true;
error = '';
try {
const formData = new FormData();
formData.append('name', name);
formData.append('symbol', symbol.toUpperCase());
if (iconFile) {
formData.append('icon', iconFile);
}
const response = await fetch('/api/coin/create', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Failed to create coin');
}
await fetchPortfolioData();
goto(`/coin/${result.coin.symbol}`);
} catch (e) {
toast.error('Failed to create coin', {
description: (e as Error).message || 'An error occurred while creating the coin'
});
} finally {
isSubmitting = false;
}
}
</script>
<svelte:head>
<title>Create Coin - Rugplay</title>
</svelte:head>
<div class="container mx-auto max-w-5xl px-4 py-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Main Form Column -->
<div class="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle class="text-lg">Coin Details</CardTitle>
</CardHeader>
<CardContent>
<form onsubmit={handleSubmit} class="space-y-6">
<!-- Icon Upload -->
<div>
<Label for="icon">Coin Icon (Optional)</Label>
<div class="mt-2 space-y-2">
<label for="icon" class="block cursor-pointer">
<div
class="border-muted-foreground/25 bg-muted/50 hover:border-muted-foreground/50 group h-24 w-24 overflow-hidden rounded-full border-2 border-dashed transition-colors"
>
<Input
id="icon"
type="file"
accept="image/*"
onchange={handleIconChange}
class="hidden"
/>
{#if iconPreview}
<img src={iconPreview} alt="Preview" class="h-full w-full object-cover" />
{:else}
<div class="flex h-full items-center justify-center">
<ImagePlus class="text-muted-foreground h-8 w-8" />
</div>
{/if}
</div>
</label>
<p class="{iconError ? 'text-destructive' : 'text-muted-foreground'} text-sm">
{#if iconError}
{iconError}
{:else if iconFile}
{iconFile.name} ({(iconFile.size / 1024).toFixed(2)} KB)
{:else}
Click to upload your coin's icon (PNG or JPG, max 1MB)
{/if}
</p>
</div>
</div>
<!-- Name Input -->
<div class="space-y-2">
<Label for="name">Coin Name</Label>
<Input id="name" type="text" bind:value={name} placeholder="e.g., Bitcoin" required />
{#if nameError}
<p class="text-destructive text-xs">{nameError}</p>
{:else}
<p class="text-muted-foreground text-sm">
Choose a memorable name for your cryptocurrency
</p>
{/if}
</div>
<!-- Symbol Input -->
<div class="space-y-2">
<Label for="symbol">Symbol</Label>
<div class="relative">
<span class="text-muted-foreground absolute left-3 top-1/2 -translate-y-1/2 text-sm"
>*</span
>
<Input
id="symbol"
type="text"
bind:value={symbol}
placeholder="BTC"
class="pl-8 uppercase"
required
/>
</div>
{#if symbolError}
<p class="text-destructive text-xs">{symbolError}</p>
{:else}
<p class="text-muted-foreground text-sm">
Short identifier for your coin (e.g., BTC for Bitcoin). Will be displayed as *{symbol ||
'SYMBOL'}
</p>
{/if}
</div>
<!-- Fair Launch Info -->
<Alert variant="default" class="bg-muted/50">
<Info class="h-4 w-4" />
<AlertDescription class="space-y-2">
<p class="font-medium">Fair Launch Settings</p>
<div class="text-muted-foreground space-y-1 text-sm">
<p>• Total Supply: <span class="font-medium">1,000,000,000 tokens</span></p>
<p>• Starting Price: <span class="font-medium">$0.000001 per token</span></p>
<p>• You receive <span class="font-medium">100%</span> of the supply</p>
<p>• Initial Market Cap: <span class="font-medium">$1,000</span></p>
<p class="mt-2 text-sm">
These settings ensure a fair start for all traders. The price will increase
naturally as people buy tokens.
</p>
</div>
</AlertDescription>
</Alert>
<!-- Submit Button -->
<Button type="submit" disabled={!canSubmit} class="w-full" size="lg">
{#if isSubmitting}
<Loader2 class="h-4 w-4 animate-spin" />
Creating...
{:else}
<Coins class="h-4 w-4" />
Create Coin (${TOTAL_COST.toFixed(2)})
{/if}
</Button>
</form>
</CardContent>
</Card>
</div>
<!-- Right Column - Preview and Info -->
<div class="space-y-4">
<!-- Cost Summary Card -->
{#if $PORTFOLIO_DATA}
<Card>
<CardHeader class="pb-2">
<div class="flex items-center justify-between">
<CardTitle class="text-base">Cost Summary</CardTitle>
<div class="text-sm">
<span class="text-muted-foreground">Balance: </span>
<span class={hasEnoughFunds ? 'text-green-600' : 'text-destructive'}>
${$PORTFOLIO_DATA.baseCurrencyBalance.toLocaleString()}
</span>
</div>
</div>
</CardHeader>
<CardContent class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Creation Fee</span>
<span>${CREATION_FEE}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Initial Liquidity</span>
<span>${INITIAL_LIQUIDITY}</span>
</div>
<Separator class="my-2" />
<div class="flex justify-between font-medium">
<span>Total Cost</span>
<span class="text-primary">${TOTAL_COST}</span>
</div>
</CardContent>
</Card>
{/if}
<!-- Info Card -->
<Card>
<CardHeader class="pb-2">
<CardTitle class="text-base">What Happens After Launch?</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-3">
<div class="flex gap-3">
<div
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
>
1
</div>
<div>
<p class="font-medium">Fair Distribution</p>
<p class="text-muted-foreground text-sm">
Everyone starts buying at the same price - no pre-sales or hidden allocations
</p>
</div>
</div>
<div class="flex gap-3">
<div
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
>
2
</div>
<div>
<p class="font-medium">Price Discovery</p>
<p class="text-muted-foreground text-sm">
Token price increases automatically as more people buy, following a bonding curve
</p>
</div>
</div>
<div class="flex gap-3">
<div
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
>
3
</div>
<div>
<p class="font-medium">Instant Trading</p>
<p class="text-muted-foreground text-sm">
Trading begins immediately - buy, sell, or distribute your tokens as you wish
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<style>
.container {
min-height: calc(100vh - 4rem);
}
</style>