feat: AI-powered prediction market (Hopium)

This commit is contained in:
Face 2025-05-28 16:44:30 +03:00
parent 4fcc55fa72
commit 2a92c37d26
33 changed files with 7009 additions and 4518 deletions

View file

@ -0,0 +1,345 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Dialog from '$lib/components/ui/dialog';
import * as Tabs from '$lib/components/ui/tabs';
import * as HoverCard from '$lib/components/ui/hover-card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Badge } from '$lib/components/ui/badge';
import { Skeleton } from '$lib/components/ui/skeleton';
import * as Avatar from '$lib/components/ui/avatar';
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
import HopiumSkeleton from '$lib/components/self/skeletons/HopiumSkeleton.svelte';
import {
TrendingUp,
TrendingDown,
Plus,
Clock,
Sparkles,
Globe,
Loader2,
CheckCircle,
XCircle
} from 'lucide-svelte';
import { USER_DATA } from '$lib/stores/user-data';
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { formatDateWithYear, formatTimeUntil, formatValue, getPublicUrl } from '$lib/utils';
import { goto } from '$app/navigation';
import type { PredictionQuestion } from '$lib/types/prediction';
let questions = $state<PredictionQuestion[]>([]);
let loading = $state(true);
let activeTab = $state('active');
let showCreateDialog = $state(false);
// Create question form
let newQuestion = $state('');
let creatingQuestion = $state(false);
let userBalance = $derived($PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance : 0);
onMount(() => {
fetchQuestions();
if ($USER_DATA) {
fetchPortfolioData();
}
});
async function fetchQuestions() {
try {
const status =
activeTab === 'active' ? 'ACTIVE' : activeTab === 'resolved' ? 'RESOLVED' : 'ALL';
// TODO: PAGINATION
const response = await fetch(`/api/hopium/questions?status=${status}&limit=50`);
if (response.ok) {
const data = await response.json();
questions = data.questions;
} else {
toast.error('Failed to load questions');
}
} catch (e) {
console.error('Failed to fetch questions:', e);
toast.error('Failed to load questions');
} finally {
loading = false;
}
}
async function createQuestion() {
if (!newQuestion.trim()) {
toast.error('Please enter a question');
return;
}
creatingQuestion = true;
try {
const response = await fetch('/api/hopium/questions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: newQuestion
})
});
const result = await response.json();
if (response.ok) {
toast.success('Question created successfully!');
showCreateDialog = false;
newQuestion = '';
fetchQuestions();
fetchPortfolioData();
} else {
toast.error(result.error || 'Failed to create question', { duration: 20000 });
}
} catch (e) {
toast.error('Network error');
} finally {
creatingQuestion = false;
}
}
function handleCreateQuestion() {
if (!$USER_DATA) {
toast.error('You must be logged in to create a question');
return;
}
if (userBalance <= 100_000) {
toast.error('You need at least $100,000 in your portfolio to create a question.');
return;
}
showCreateDialog = true;
}
$effect(() => {
if (activeTab) {
loading = true;
fetchQuestions();
}
});
</script>
<svelte:head>
<title>Hopium - Prediction Market | Rugplay</title>
<meta
name="description"
content="Create and bet on prediction markets with AI-powered resolution"
/>
</svelte:head>
<!-- Create Question Dialog -->
<Dialog.Root bind:open={showCreateDialog}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Sparkles class="h-5 w-5" />
Create
</Dialog.Title>
<Dialog.Description>Create a yes/no question that will be resolved by AI.</Dialog.Description>
</Dialog.Header>
<div class="space-y-4">
<div class="space-y-2">
<Label for="question">Question *</Label>
<Input
id="question"
bind:value={newQuestion}
placeholder="Will *SKIBIDI reach $100 price today?"
maxlength={200}
/>
<p class="text-muted-foreground text-xs">{newQuestion.length}/200 characters</p>
<p class="text-muted-foreground text-xs">
The AI will automatically determine the appropriate resolution date and criteria.
</p>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (showCreateDialog = false)}>Cancel</Button>
<Button onclick={createQuestion} disabled={creatingQuestion || !newQuestion.trim()}>
{#if creatingQuestion}
<Loader2 class="h-4 w-4 animate-spin" />
Processing...
{:else}
Publish
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<div class="container mx-auto max-w-7xl p-6">
<header class="mb-8">
<div class="text-center">
<h1 class="mb-2 flex items-center justify-center gap-2 text-3xl font-bold">
<Sparkles class="h-8 w-8 text-purple-500" />
Hopium<span class="text-xs">[BETA]</span>
</h1>
<p class="text-muted-foreground mb-6">
AI-powered prediction markets. Create questions and bet on outcomes.
</p>
</div>
</header>
<Tabs.Root bind:value={activeTab} class="w-full">
<div class="mb-6 flex items-center justify-center gap-2">
<Tabs.List class="grid w-full max-w-md grid-cols-3">
<Tabs.Trigger value="active">Active</Tabs.Trigger>
<Tabs.Trigger value="resolved">Resolved</Tabs.Trigger>
<Tabs.Trigger value="all">All</Tabs.Trigger>
</Tabs.List>
{#if $USER_DATA}
<Button onclick={handleCreateQuestion}>
<Plus class="h-4 w-4" />
Ask
</Button>
{/if}
</div>
<Tabs.Content value={activeTab}>
{#if loading}
<HopiumSkeleton />
{:else if questions.length === 0}
<div class="py-16 text-center">
<h3 class="mb-2 text-lg font-semibold">No questions yet</h3>
<p class="text-muted-foreground mb-6">Be the first to create a prediction question!</p>
</div>
{:else}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each questions as question}
<Card.Root
class="bg-card hover:bg-card/90 flex cursor-pointer flex-col transition-colors"
onclick={() => goto(`/hopium/${question.id}`)}
>
<Card.Header class="pb-4">
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<h3 class="break-words text-lg font-medium">
{question.question}
</h3>
</div>
<div class="flex flex-col items-end gap-2">
{#if question.status === 'RESOLVED'}
<Badge
variant={question.aiResolution ? 'default' : 'destructive'}
class="flex flex-shrink-0 items-center gap-1"
>
{#if question.aiResolution}
<CheckCircle class="h-3 w-3" />
YES
{:else}
<XCircle class="h-3 w-3" />
NO
{/if}
</Badge>
{/if}
<!-- Probability Meter -->
<div class="relative flex h-12 w-16 items-end justify-center">
<svg class="h-10 w-16" viewBox="0 0 64 32">
<!-- Background arc -->
<path
d="M 8 28 A 24 24 0 0 1 56 28"
fill="none"
stroke="var(--muted-foreground)"
stroke-width="3"
stroke-linecap="round"
opacity="0.3"
/>
<!-- Progress arc -->
<path
d="M 8 28 A 24 24 0 0 1 56 28"
fill="none"
stroke="var(--primary)"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray={Math.PI * 24}
stroke-dashoffset={Math.PI * 24 -
(question.yesPercentage / 100) * Math.PI * 24}
class="transition-all duration-300 ease-in-out"
/>
</svg>
<div class="absolute bottom-0 text-sm font-medium">
{question.yesPercentage.toFixed(0)}%
</div>
</div>
</div>
</div>
<div class="text-muted-foreground flex items-center gap-2 text-sm">
<div class="flex items-center gap-1">
<Clock class="h-3 w-3" />
{#if question.status === 'ACTIVE'}
{formatTimeUntil(question.resolutionDate)} remaining
{:else}
Resolved {formatDateWithYear(question.resolvedAt || '')}
{/if}
</div>
<span></span>
<div class="flex items-center gap-1">
{formatValue(question.totalAmount)}
</div>
{#if question.requiresWebSearch}
<span></span>
<Globe class="h-3 w-3 text-blue-500" />
{/if}
</div>
<div class="mb-2 mt-2 flex items-center gap-2 text-sm">
<HoverCard.Root>
<HoverCard.Trigger>
<button
class="flex cursor-pointer items-center gap-2 text-left hover:underline"
>
<Avatar.Root class="h-5 w-5">
<Avatar.Image
src={getPublicUrl(question.creator.image)}
alt={question.creator.name}
/>
<Avatar.Fallback class="text-xs"
>{question.creator.name.charAt(0)}</Avatar.Fallback
>
</Avatar.Root>
<span class="text-muted-foreground">@{question.creator.username}</span>
</button>
</HoverCard.Trigger>
<HoverCard.Content class="w-80">
<UserProfilePreview userId={question.creator.id} />
</HoverCard.Content>
</HoverCard.Root>
</div>
<!-- User's bet amounts if they have any -->
{#if question.userBets && (question.userBets.yesAmount > 0 || question.userBets.noAmount > 0)}
<div class="text-muted-foreground flex items-center gap-4 text-sm">
<span>Your bets:</span>
{#if question.userBets.yesAmount > 0}
<div class="flex items-center gap-1">
<TrendingUp class="h-3 w-3 text-green-600" />
<span class="text-green-600"
>YES: ${question.userBets.yesAmount.toFixed(2)}</span
>
</div>
{/if}
{#if question.userBets.noAmount > 0}
<div class="flex items-center gap-1">
<TrendingDown class="h-3 w-3 text-red-600" />
<span class="text-red-600"
>NO: ${question.userBets.noAmount.toFixed(2)}</span
>
</div>
{/if}
</div>
{/if}
</Card.Header>
</Card.Root>
{/each}
</div>
{/if}
</Tabs.Content>
</Tabs.Root>
</div>

View file

@ -0,0 +1,615 @@
<script lang="ts">
import { page } from '$app/state';
import { goto } from '$app/navigation';
import * as Card from '$lib/components/ui/card';
import * as HoverCard from '$lib/components/ui/hover-card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Badge } from '$lib/components/ui/badge';
import * as Avatar from '$lib/components/ui/avatar';
import { Separator } from '$lib/components/ui/separator';
import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte';
import {
Loader2,
CheckCircle,
XCircle,
Calculator,
History,
ChartColumn,
MessageCircleQuestion
} from 'lucide-svelte';
import { USER_DATA } from '$lib/stores/user-data';
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { formatDateWithYear, getPublicUrl, formatTimeUntil } from '$lib/utils';
import { createChart, ColorType, type IChartApi, LineSeries } from 'lightweight-charts';
import type { PredictionQuestion } from '$lib/types/prediction';
import HopiumQuestionSkeleton from '$lib/components/self/skeletons/HopiumQuestionSkeleton.svelte';
let question = $state<PredictionQuestion | null>(null);
let loading = $state(true);
let probabilityData = $state<any[]>([]);
// Betting form
let betSide = $state<boolean>(true);
let placingBet = $state(false);
let customBetAmount = $state('');
let userBalance = $derived($PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance : 0);
let questionId = $derived(parseInt(page.params.id));
// Chart related
let chartContainer = $state<HTMLDivElement>();
let chart: IChartApi | null = null;
let lineSeries: any = null;
onMount(() => {
fetchQuestion();
if ($USER_DATA) {
fetchPortfolioData();
}
});
async function fetchQuestion() {
try {
const response = await fetch(`/api/hopium/questions/${questionId}`);
if (response.ok) {
const result = await response.json();
question = result.question || result;
probabilityData = result.probabilityHistory || [];
} else if (response.status === 404) {
toast.error('Question not found');
goto('/hopium');
} else {
toast.error('Failed to load question');
}
} catch (e) {
console.error('Failed to fetch question:', e);
toast.error('Failed to load question');
} finally {
loading = false;
}
}
async function placeBet() {
if (!question || !customBetAmount || Number(customBetAmount) <= 0) {
toast.error('Please enter a valid bet amount');
return;
}
placingBet = true;
try {
const response = await fetch(`/api/hopium/questions/${question.id}/bet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
side: betSide,
amount: Number(customBetAmount)
})
});
const result = await response.json();
if (response.ok) {
toast.success(
`Bet placed! Potential winnings: $${result.bet.potentialWinnings.toFixed(2)}`
);
customBetAmount = '';
fetchQuestion();
fetchPortfolioData();
} else {
toast.error(result.error || 'Failed to place bet');
}
} catch (e) {
toast.error('Network error');
} finally {
placingBet = false;
}
}
$effect(() => {
if (chart && probabilityData.length > 0) {
chart.remove();
chart = null;
}
if (chartContainer && probabilityData.length > 0 && question) {
chart = createChart(chartContainer, {
layout: {
textColor: '#666666',
background: { type: ColorType.Solid, color: 'transparent' },
attributionLogo: false
},
grid: {
vertLines: { color: '#2B2B43' },
horzLines: { color: '#2B2B43' }
},
rightPriceScale: {
borderVisible: false,
scaleMargins: { top: 0.1, bottom: 0.1 },
alignLabels: true,
entireTextOnly: false,
visible: true
},
timeScale: {
borderVisible: false,
timeVisible: true,
rightOffset: 5
},
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 }
}
});
lineSeries = chart.addSeries(LineSeries, {
color: '#2962FF',
lineWidth: 3,
priceFormat: {
type: 'custom',
formatter: (price: number) => `${price.toFixed(1)}%`
}
});
lineSeries.setData(probabilityData);
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;
}
};
}
});
let estimatedYesPayout = $derived(
!question?.userBets?.yesAmount || question.userBets.yesAmount <= 0
? 0
: question.userBets.estimatedYesWinnings || 0
);
let estimatedNoPayout = $derived(
!question?.userBets?.noAmount || question.userBets.noAmount <= 0
? 0
: question.userBets.estimatedNoWinnings || 0
);
let estimatedWin = $derived(
(() => {
const amount = Number(customBetAmount);
if (!amount || amount <= 0 || !question) return 0;
const totalPool = question.yesAmount + question.noAmount + amount;
const relevantPool = betSide ? question.yesAmount + amount : question.noAmount + amount;
return relevantPool > 0 ? (totalPool / relevantPool) * amount : 0;
})()
);
</script>
<svelte:head>
{#if question}
<title>{question.question} - Rugplay</title>
<meta name="description" content={question.description || question.question} />
{:else}
<title>Hopium - Rugplay</title>
{/if}
</svelte:head>
<div class="container mx-auto max-w-7xl p-6">
{#if loading}
<HopiumQuestionSkeleton />
{:else if !question}
<div class="flex h-96 items-center justify-center">
<div class="text-center">
<h3 class="mb-2 text-xl font-semibold">Question not found</h3>
<p class="text-muted-foreground mb-6">
This question may have been removed or doesn't exist.
</p>
</div>
</div>
{:else}
<div class="flex items-center gap-3">
<div class="bg-muted rounded-lg p-4">
<MessageCircleQuestion class="h-14 w-14" />
</div>
<div class="flex-1">
<h1 class="text-2xl font-semibold">{question.question}</h1>
{#if question.status === 'ACTIVE'}
<p class="text-muted-foreground mt-1 text-sm">
Ends in {formatTimeUntil(question.resolutionDate)}
</p>
{/if}
{#if question.status === 'RESOLVED'}
<Badge variant="destructive" class={question.aiResolution ? 'bg-success/80!' : ''}>
{#if question.aiResolution}
<CheckCircle class="h-4 w-4" />
RESOLVED: YES
{:else}
<XCircle class="h-4 w-4" />
RESOLVED: NO
{/if}
</Badge>
{/if}
</div>
</div>
<div class="text-muted-foreground mb-4 mt-3 flex flex-wrap items-center gap-1.5 text-xs">
<span>Created by</span>
<HoverCard.Root>
<HoverCard.Trigger
class="flex cursor-pointer items-center gap-1 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
onclick={() => goto(`/user/${question?.creator.username}`)}
>
<Avatar.Root class="h-4 w-4">
<Avatar.Image
src={getPublicUrl(question.creator.image)}
alt={question.creator.username}
/>
<Avatar.Fallback>{question.creator.username.charAt(0)}</Avatar.Fallback>
</Avatar.Root>
<span class="font-medium">{question.creator.name} (@{question.creator.username})</span>
</HoverCard.Trigger>
<HoverCard.Content class="w-80" side="bottom" sideOffset={3}>
<UserProfilePreview userId={question.creator.id} />
</HoverCard.Content>
</HoverCard.Root>
</div>
<div class="grid gap-8">
<!-- Main content grid with better spacing -->
<div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
<!-- Left: Chart (2/3 width) -->
<div class="lg:col-span-2">
<Card.Root class="shadow-sm">
<Card.Header>
<div class="flex items-center justify-between">
<Card.Title class="flex items-center gap-3 text-xl font-bold">
<ChartColumn class="h-6 w-6" />
Chart
</Card.Title>
<div class="text-right">
<div class="text-success text-4xl font-bold">
{question.yesPercentage.toFixed(1)}%
</div>
<div class="text-muted-foreground text-sm font-medium">YES chance</div>
</div>
</div>
</Card.Header>
<Card.Content>
{#if probabilityData.length === 0}
<div
class="border-muted flex h-[400px] items-center justify-center rounded-lg border-2 border-dashed"
>
<div class="text-center">
<ChartColumn class="text-muted-foreground mx-auto mb-3 h-12 w-12" />
<p class="text-muted-foreground text-sm">Chart will appear after first bet</p>
</div>
</div>
{:else}
<div class="h-[400px] w-full rounded-lg border" bind:this={chartContainer}></div>
{/if}
</Card.Content>
</Card.Root>
</div>
<!-- Right: Trading Controls (1/3 width) -->
<div class="space-y-6 lg:col-span-1">
<!-- Trading Card -->
<Card.Root>
<Card.Header>
<Card.Title>Place Bet</Card.Title>
</Card.Header>
<Card.Content class="space-y-6">
<!-- YES/NO Buttons -->
<div class="grid grid-cols-2 gap-4">
<Button
class={betSide
? 'bg-success/80 hover:bg-success/90 w-full'
: 'bg-muted hover:bg-muted/90 w-full'}
size="lg"
onclick={() => (betSide = true)}
disabled={question.aiResolution !== null}
>
<div class="flex items-baseline gap-2">
<span class="text-xl font-bold">YES</span>
<span class="text-sm">{question.yesPercentage.toFixed(1)}¢</span>
</div>
</Button>
<Button
class={!betSide
? 'bg-destructive hover:bg-destructive/90 w-full'
: 'bg-muted hover:bg-muted/90 w-full'}
size="lg"
onclick={() => (betSide = false)}
disabled={question.aiResolution !== null}
>
<div class="flex items-baseline gap-2">
<span class="text-xl font-bold">NO</span>
<span class="text-sm">{question.noPercentage.toFixed(1)}¢</span>
</div>
</Button>
</div>
<!-- Amount Input -->
<div class="space-y-2">
<Input
type="number"
step="0.01"
min="0.01"
placeholder="Enter amount..."
bind:value={customBetAmount}
disabled={question.aiResolution !== null}
/>
</div>
<!-- Quick Amount Buttons -->
<div class="flex gap-2">
<Button
variant="outline"
class="flex-1"
onclick={() => (customBetAmount = '1')}
disabled={question.aiResolution !== null}
>
$1
</Button>
<Button
variant="outline"
class="flex-1"
onclick={() => (customBetAmount = '20')}
disabled={question.aiResolution !== null}
>
$20
</Button>
<Button
variant="outline"
class="flex-1"
onclick={() => (customBetAmount = '100')}
disabled={question.aiResolution !== null}
>
$100
</Button>
<Button
variant="outline"
class="flex-1"
onclick={() => (customBetAmount = userBalance.toString())}
disabled={question.aiResolution !== null}
>
Max
</Button>
</div>
<!-- Win Estimation -->
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">To win:</span>
<span class="font-mono">
${estimatedWin.toFixed(2)}
</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Balance:</span>
<span class="font-mono">
${userBalance.toFixed(2)}
</span>
</div>
</div>
<!-- Pay Button -->
<Button
class="w-full"
size="lg"
disabled={!customBetAmount ||
Number(customBetAmount) <= 0 ||
Number(customBetAmount) > userBalance ||
placingBet ||
question.aiResolution !== null}
onclick={placeBet}
>
{#if placingBet}
<Loader2 class="h-4 w-4 animate-spin" />
Placing Bet...
{:else}
Pay ${Number(customBetAmount || 0).toFixed(2)}
{/if}
</Button>
</Card.Content>
</Card.Root>
{#if !$USER_DATA}
<Card.Root class="shadow-sm">
<Card.Header>
<Card.Title class="text-lg font-bold">Start Betting</Card.Title>
</Card.Header>
<Card.Content class="pb-6">
<div class="py-6 text-center">
<p class="text-muted-foreground mb-4 text-sm">Sign in to place bets</p>
<Button size="lg" onclick={() => goto('/')}>Sign In</Button>
</div>
</Card.Content>
</Card.Root>
{/if}
</div>
</div>
<!-- Position and Stats Cards below chart -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- User Position Card (if they have bets) -->
{#if $USER_DATA && question.userBets && question.userBets.totalAmount && question.userBets.totalAmount > 0}
<Card.Root class="gap-1">
<Card.Header>
<Card.Title class="flex items-center gap-3 text-lg font-bold">
<div class="bg-muted rounded-full p-2">
<Calculator class="h-5 w-5" />
</div>
Your Position
</Card.Title>
</Card.Header>
<Card.Content class="pb-4">
<div class="space-y-3">
{#if question.userBets.yesAmount > 0}
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-green-600">YES Bet</div>
<div class="text-muted-foreground text-xs">
Payout: ${estimatedYesPayout.toFixed(2)}
</div>
</div>
<div class="text-lg font-bold text-green-600">
${question.userBets.yesAmount.toFixed(2)}
</div>
</div>
{/if}
{#if question.userBets.noAmount > 0}
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-red-600">NO Bet</div>
<div class="text-muted-foreground text-xs">
Payout: ${estimatedNoPayout.toFixed(2)}
</div>
</div>
<div class="text-lg font-bold text-red-600">
${question.userBets.noAmount.toFixed(2)}
</div>
</div>
{/if}
{#if question.userBets.yesAmount > 0 && question.userBets.noAmount > 0}
<Separator />
{/if}
<div class="flex items-center justify-between">
<span class="text-muted-foreground text-sm font-medium">Total Invested</span>
<span class="text-lg font-bold">${question.userBets.totalAmount.toFixed(2)}</span>
</div>
</div>
</Card.Content>
</Card.Root>
{:else if $USER_DATA}
<Card.Root class="gap-1">
<Card.Header>
<Card.Title>
<div class="inline-flex items-center gap-2">
<Calculator class="h-5 w-5" />
Place Your Bet
</div>
</Card.Title>
</Card.Header>
<Card.Content>
{#if question.status === 'ACTIVE'}
<p class="text-muted-foreground mb-6 text-sm">You haven't placed any bets yet</p>
{:else}
<div class="py-6 text-center">
<p class="text-muted-foreground text-sm">This question has been resolved</p>
</div>
{/if}
</Card.Content>
</Card.Root>
{/if}
<!-- Market Stats Card -->
<Card.Root class="gap-1">
<Card.Header>
<Card.Title class="flex items-center gap-3 text-lg font-bold">
<div class="bg-muted rounded-full p-2">
<ChartColumn class="h-5 w-5" />
</div>
Market Stats
</Card.Title>
</Card.Header>
<Card.Content>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Total Volume:</span>
<span class="font-mono text-sm">
${question.totalAmount.toFixed(2)}
</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Total Bets:</span>
<span class="font-mono text-sm">
{question.recentBets?.length || 0}
</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Created:</span>
<span class="font-mono text-sm">
{formatDateWithYear(question.createdAt)}
</span>
</div>
{#if question.status === 'ACTIVE'}
<div class="flex justify-between">
<span class="text-muted-foreground text-sm">Resolves:</span>
<span class="font-mono text-sm">
{formatDateWithYear(question.resolutionDate)}
</span>
</div>
{/if}
</Card.Content>
</Card.Root>
</div>
<!-- Recent Activity Section -->
{#if question.recentBets && question.recentBets.length > 0}
<Card.Root class="shadow-sm">
<Card.Header>
<Card.Title class="flex items-center gap-3 text-xl font-bold">
<div class="bg-muted rounded-full p-2">
<History class="h-6 w-6" />
</div>
Recent Activity
</Card.Title>
</Card.Header>
<Card.Content class="pb-6">
<div class="space-y-4">
{#each question.recentBets as bet}
<div class="flex items-center justify-between rounded-xl border p-4">
<div class="flex items-center gap-4">
<HoverCard.Root>
<HoverCard.Trigger>
<button
class="flex cursor-pointer items-center gap-3 text-left"
onclick={() => goto(`/user/${bet.user.username}`)}
>
<Avatar.Root class="h-10 w-10">
<Avatar.Image src={getPublicUrl(bet.user.image)} alt={bet.user.name} />
<Avatar.Fallback class="text-sm"
>{bet.user.name.charAt(0)}</Avatar.Fallback
>
</Avatar.Root>
<div>
<div class="font-semibold hover:underline">{bet.user.name}</div>
<div class="text-muted-foreground text-sm">@{bet.user.username}</div>
</div>
</button>
</HoverCard.Trigger>
<HoverCard.Content class="w-80">
<UserProfilePreview userId={bet.user.id} />
</HoverCard.Content>
</HoverCard.Root>
<Badge variant="destructive" class={bet.side ? 'bg-success/80!' : ''}>
{bet.side ? 'YES' : 'NO'}
</Badge>
</div>
<div class="text-right">
<div class="text-lg font-bold">${bet.amount.toFixed(2)}</div>
<div class="text-muted-foreground text-sm">
{new Date(bet.createdAt).toLocaleDateString()}
</div>
</div>
</div>
{/each}
</div>
</Card.Content>
</Card.Root>
{/if}
</div>
{/if}
</div>