feat: betting history with P&L tracking on bot profile
BetHistory component shows personal bet history with fight links, amounts, outcomes, and payouts. P&L summary shows total wagered, total won, and net sats. Added to BotProfilePage for the bot owner's view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
005a7942fb
commit
5e598e3987
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{ pubkey: string }>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Bet {
|
||||
id: string
|
||||
fightId: string
|
||||
botId: string
|
||||
amountSats: number
|
||||
oddsAtPlacement: number
|
||||
status: string
|
||||
payoutSats: number | null
|
||||
createdAt: string
|
||||
settledAt: string | null
|
||||
}
|
||||
|
||||
const bets = ref<Bet[]>([])
|
||||
const isLoading = ref(true)
|
||||
|
||||
const totalWagered = computed(() => bets.value.reduce((sum, b) => sum + b.amountSats, 0))
|
||||
const totalWon = computed(() => bets.value.reduce((sum, b) => sum + (b.payoutSats || 0), 0))
|
||||
const netSats = computed(() => totalWon.value - totalWagered.value)
|
||||
|
||||
function statusColor(status: string): string {
|
||||
if (status === 'won') return 'text-neon-green'
|
||||
if (status === 'lost') return 'text-ko'
|
||||
if (status === 'refunded') return 'text-neon-yellow'
|
||||
return 'text-text-muted'
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
if (status === 'won') return 'WON'
|
||||
if (status === 'lost') return 'LOST'
|
||||
if (status === 'refunded') return 'REFUND'
|
||||
if (status === 'locked') return 'ACTIVE'
|
||||
return status.toUpperCase()
|
||||
}
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const diffSec = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000)
|
||||
if (diffSec < 60) return `${diffSec}s`
|
||||
const diffMin = Math.floor(diffSec / 60)
|
||||
if (diffMin < 60) return `${diffMin}m`
|
||||
const diffHr = Math.floor(diffMin / 60)
|
||||
if (diffHr < 24) return `${diffHr}h`
|
||||
return `${Math.floor(diffHr / 24)}d`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/bets/history/${props.pubkey}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
bets.value = data.bets || []
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
isLoading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="font-display font-black text-xs tracking-widest text-neon-purple mb-2">BET HISTORY</h3>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center py-4">
|
||||
<div class="w-6 h-6 border-2 border-neon-purple/30 border-t-neon-purple rounded-full animate-spin" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="bets.length === 0" class="py-4">
|
||||
<p class="font-mono text-text-muted text-xs text-center">No bets placed yet.</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- P&L summary -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
<div class="flex-1 px-2 py-1.5 bg-surface border border-border rounded-md text-center">
|
||||
<p class="font-pixel text-[8px] text-text-muted tracking-wider">WAGERED</p>
|
||||
<p class="font-mono text-xs text-text-primary">{{ totalWagered }}</p>
|
||||
</div>
|
||||
<div class="flex-1 px-2 py-1.5 bg-surface border border-border rounded-md text-center">
|
||||
<p class="font-pixel text-[8px] text-text-muted tracking-wider">WON</p>
|
||||
<p class="font-mono text-xs text-neon-green">{{ totalWon }}</p>
|
||||
</div>
|
||||
<div class="flex-1 px-2 py-1.5 bg-surface border border-border rounded-md text-center">
|
||||
<p class="font-pixel text-[8px] text-text-muted tracking-wider">NET</p>
|
||||
<p class="font-mono text-xs" :class="netSats >= 0 ? 'text-neon-green' : 'text-ko'">
|
||||
{{ netSats >= 0 ? '+' : '' }}{{ netSats }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bet list -->
|
||||
<div class="space-y-1.5 max-h-48 overflow-y-auto">
|
||||
<button
|
||||
v-for="bet in bets"
|
||||
:key="bet.id"
|
||||
class="w-full text-left px-2.5 py-1.5 bg-surface/50 border border-border rounded-md
|
||||
hover:border-neon-cyan/20 transition-all text-xs"
|
||||
@click="router.push(`/arena/${bet.fightId}`)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-mono text-text-primary">{{ bet.amountSats }} sats</span>
|
||||
<span class="font-display font-bold text-[10px] tracking-wider" :class="statusColor(bet.status)">
|
||||
{{ statusLabel(bet.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5">
|
||||
<span v-if="bet.payoutSats" class="font-mono text-[10px] text-neon-green">
|
||||
+{{ bet.payoutSats }} sats
|
||||
</span>
|
||||
<span v-else class="font-mono text-[10px] text-text-muted">
|
||||
{{ (bet.oddsAtPlacement * 0.95).toFixed(2) }}x odds
|
||||
</span>
|
||||
<span class="font-pixel text-[8px] text-text-muted">{{ timeAgo(bet.createdAt) }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { useNostr } from '../composables/useNostr'
|
||||
import SpritePreview from '../components/SpritePreview.vue'
|
||||
import HumanPreview from '../components/HumanPreview.vue'
|
||||
import WalletConnect from '../components/WalletConnect.vue'
|
||||
import BetHistory from '../components/BetHistory.vue'
|
||||
import type { SpriteCustomization } from '../game/sprites'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -701,9 +702,9 @@ const tierClass = (t: number) => `tier-${t}`
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Recent bouts (scrollable) -->
|
||||
<div class="flex-1 min-w-0 flex flex-col min-h-0 mt-6 lg:mt-0">
|
||||
<div class="border border-border bg-surface-raised/30 flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
<!-- RIGHT COLUMN: Recent bouts + Bet history (scrollable) -->
|
||||
<div class="flex-1 min-w-0 flex flex-col min-h-0 mt-6 lg:mt-0 gap-4">
|
||||
<div class="border border-border bg-surface-raised/30 flex flex-col min-h-0 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-border/50 flex-shrink-0">
|
||||
<p class="font-display text-[10px] font-bold text-text-muted tracking-[0.15em]">
|
||||
RECENT BOUTS
|
||||
@@ -729,6 +730,11 @@ const tierClass = (t: number) => `tier-${t}`
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bet history (owner's view) -->
|
||||
<div v-if="isOwner && pubkey" class="border border-border bg-surface-raised/30 p-3">
|
||||
<BetHistory :pubkey="pubkey" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user