Files
BetiX/resources/js/pages/Wallet.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

2159 lines
70 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import {
Wallet, ArrowDownCircle, ArrowUpCircle, History, Dices,
Bitcoin, Coins, Layers, CircleDollarSign, Zap, Bone, Gem,
CircleDot, Hexagon, PawPrint, Waypoints, Triangle, BadgeCent,
CreditCard, Loader2, Info, AlertCircle, Copy, QrCode,
ExternalLink, CheckCircle, AlertTriangle, Search,
ArrowUpDown, ChevronLeft, ChevronRight, Clock,
ArrowDownToLine, ShieldCheck, ArrowRight, WalletMinimal,
SlidersHorizontal, X, RotateCcw, ArrowUpToLine
} from 'lucide-vue-next';
import VaultModal from '@/components/vault/VaultModal.vue';
import { csrfFetch } from '@/utils/csrfFetch';
import UserLayout from '../layouts/user/userlayout.vue';
// Props from Controller
const props = defineProps<{
wallets: any[];
btxBalance: string;
}>();
// NOWPayments Deposit UI state
const depositLoading = ref(false);
const depositError = ref<string | null>(null);
const currencies = ref<{ mode: string; enabled: string[]; limits: { global_min_usd: number; global_max_usd: number }; overrides: Record<string, any>; btx_per_usd: number } | null>(null);
const selectedPayCurrency = ref<string>('BTC');
const amountInput = ref<string>('50');
const startedOrder = ref<{ order_id: string; invoice_id: string; pay_currency: string; pay_address?: string | null; redirect_url?: string | null } | null>(null);
const statusMsg = ref<string | null>(null);
let pollTimer: any = null;
// History State
const paymentHistory = ref<any[]>([]);
const historyLoading = ref(false);
const historyFilter = ref<'all' | 'finished' | 'pending' | 'failed' | 'canceled'>('all');
const historyTypeFilter = ref<'all' | 'deposit' | 'withdrawal'>('all');
const historyCurrencyFilter = ref<string>('all');
const historyDateFrom = ref<string>('');
const historyDateTo = ref<string>('');
const historyAmountMin = ref<string>('');
const historyAmountMax = ref<string>('');
const historyShowFilters = ref(false);
const historyAvailableCurrencies = computed(() => {
const set = new Set<string>();
paymentHistory.value.forEach(i => { if (i.pay_currency) set.add(i.pay_currency); });
return Array.from(set).sort();
});
const activeFilterCount = computed(() => {
let n = 0;
if (historyFilter.value !== 'all') n++;
if (historyTypeFilter.value !== 'all') n++;
if (historyCurrencyFilter.value !== 'all') n++;
if (historyDateFrom.value) n++;
if (historyDateTo.value) n++;
if (historyAmountMin.value) n++;
if (historyAmountMax.value) n++;
return n;
});
const resetHistoryFilters = () => {
historyFilter.value = 'all';
historyTypeFilter.value = 'all';
historyCurrencyFilter.value = 'all';
historyDateFrom.value = '';
historyDateTo.value = '';
historyAmountMin.value = '';
historyAmountMax.value = '';
};
// Bets State
const betsHistory = ref<any[]>([]);
const betsLoading = ref(false);
const betsSearch = ref('');
const betsSort = ref('created_at');
const betsOrder = ref<'desc' | 'asc'>('desc');
const betsPage = ref(1);
const betsTotalPages = ref(1);
// Dynamic wallets based on enabled currencies
const balances = ref<any[]>([]);
const activeTab = ref('deposit');
const selectedCurrency = ref<any>(null);
const showVault = ref(false);
// Icon map for dynamic coin icons
const iconMap: Record<string, any> = {
'wallet': Wallet,
'bitcoin': Bitcoin,
'coins': Coins,
'layers': Layers,
'circle-dollar-sign': CircleDollarSign,
'zap': Zap,
'bone': Bone,
'gem': Gem,
'circle-dot': CircleDot,
'hexagon': Hexagon,
'paw-print': PawPrint,
'waypoints': Waypoints,
'triangle': Triangle,
'badge-cent': BadgeCent,
};
const getIconComp = (name: string) => iconMap[name] || BadgeCent;
const currencyMetadata: Record<string, { name: string; icon: string; color: string }> = {
'BTC': { name: 'Bitcoin', icon: 'bitcoin', color: '#f7931a' },
'ETH': { name: 'Ethereum', icon: 'coins', color: '#627eea' },
'LTC': { name: 'Litecoin', icon: 'coins', color: '#345d9d' },
'SOL': { name: 'Solana', icon: 'layers', color: '#00ff9d' },
'USDT_TRC20': { name: 'Tether (TRC20)', icon: 'circle-dollar-sign', color: '#26a17b' },
'USDT_ERC20': { name: 'Tether (ERC20)', icon: 'circle-dollar-sign', color: '#26a17b' },
'XRP': { name: 'Ripple', icon: 'zap', color: '#23292f' },
'DOGE': { name: 'Dogecoin', icon: 'bone', color: '#c2a633' },
'ADA': { name: 'Cardano', icon: 'coins', color: '#0033ad' },
'TRX': { name: 'Tron', icon: 'gem', color: '#ff060a' },
'BNB': { name: 'Binance Coin', icon: 'circle-dot', color: '#f3ba2f' },
'MATIC': { name: 'BNB Smart Chain', icon: 'hexagon', color: '#f3ba2f' },
'BCH': { name: 'Bitcoin Cash', icon: 'bitcoin', color: '#8dc351' },
'SHIB': { name: 'Shiba Inu', icon: 'paw-print', color: '#e1b303' },
'DOT': { name: 'Polygon', icon: 'waypoints', color: '#8247e5' },
'AVAX': { name: 'Avalanche', icon: 'triangle', color: '#e84142' },
};
const getCurrencyMeta = (ticker: string) => {
return currencyMetadata[ticker] || { name: ticker, icon: 'badge-cent', color: '#a1a1aa' };
};
const initBalances = () => {
const list: any[] = [];
if (currencies.value && currencies.value.enabled) {
currencies.value.enabled.forEach(ticker => {
const meta = getCurrencyMeta(ticker);
list.push({
currency: ticker,
name: meta.name,
amount: 0.00000,
icon: meta.icon,
color: meta.color,
});
});
} else {
list.push(
{ currency: 'BTC', name: 'Bitcoin', amount: 0.00000, icon: 'bitcoin', color: '#f7931a' },
{ currency: 'ETH', name: 'Ethereum', amount: 0.000, icon: 'coins', color: '#627eea' },
{ currency: 'SOL', name: 'Solana', amount: 0.0, icon: 'layers', color: '#00ff9d' }
);
}
list.push({ currency: 'BTX', name: 'BetiX Coin', amount: parseFloat(props.btxBalance || '0'), icon: 'gem', color: '#ff007a' });
if (props.wallets) {
props.wallets.forEach(w => {
let item = list.find(l => l.currency === w.currency);
if (!item) {
item = list.find(l => l.currency.replace('_', '') === w.currency);
}
if (item) {
item.amount = parseFloat(w.balance);
}
});
}
balances.value = list;
const cryptos = balances.value.filter(b => b.currency !== 'BTX');
if (cryptos.length > 0) {
selectedCurrency.value = cryptos.reduce((prev, current) => (prev.amount > current.amount) ? prev : current);
} else if (balances.value.length > 0) {
selectedCurrency.value = balances.value[0];
}
};
watch(() => props.btxBalance, (newVal) => {
const btx = balances.value.find(b => b.currency === 'BTX');
if (btx) {
btx.amount = parseFloat(newVal || '0');
}
});
watch(activeTab, (newTab) => {
if (newTab === 'history') loadHistory();
else if (newTab === 'bets') loadBets();
});
watch([betsSearch, betsSort, betsOrder, betsPage], () => {
if (activeTab.value === 'bets') loadBets();
});
const selectCurrency = (coin: any) => {
selectedCurrency.value = coin;
if (currencies.value?.enabled?.includes(coin.currency)) {
selectedPayCurrency.value = coin.currency;
}
};
function copyToClipboard(text: string) {
try {
navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
async function loadCurrencies() {
try {
const resp = await csrfFetch('/wallet/deposits/currencies', { method: 'GET', headers: { 'Accept': 'application/json' } });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
currencies.value = json;
initBalances();
const want = selectedCurrency.value?.currency || 'BTC';
if (json.enabled?.includes(want)) {
selectedPayCurrency.value = want;
} else if (json.enabled?.length) {
selectedPayCurrency.value = json.enabled[0];
}
} catch (e: any) {
depositError.value = e?.message || 'Konnte Währungen nicht laden';
initBalances();
}
}
async function loadHistory() {
historyLoading.value = true;
try {
const resp = await csrfFetch('/wallet/deposits/history', { method: 'GET', headers: { 'Accept': 'application/json' } });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
paymentHistory.value = json.data || [];
} catch (e: any) {
console.error('Failed to load history', e);
} finally {
historyLoading.value = false;
}
}
async function loadBets() {
betsLoading.value = true;
try {
const query = new URLSearchParams({
search: betsSearch.value,
sort: betsSort.value,
order: betsOrder.value,
page: betsPage.value.toString()
});
const resp = await csrfFetch(`/wallet/bets?${query.toString()}`, { method: 'GET', headers: { 'Accept': 'application/json' } });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
betsHistory.value = json.data || [];
betsTotalPages.value = json.last_page || 1;
} catch (e: any) {
console.error('Failed to load bets', e);
} finally {
betsLoading.value = false;
}
}
const toggleBetsSort = (field: string) => {
if (betsSort.value === field) {
betsOrder.value = betsOrder.value === 'asc' ? 'desc' : 'asc';
} else {
betsSort.value = field;
betsOrder.value = 'desc';
}
betsPage.value = 1;
};
const filteredHistory = computed(() => {
return paymentHistory.value.filter(item => {
const s = item.status.toLowerCase();
// Status
if (historyFilter.value === 'finished' && s !== 'finished') return false;
if (historyFilter.value === 'pending' && !['waiting', 'new', 'confirming'].includes(s)) return false;
if (historyFilter.value === 'failed' && !['failed', 'expired'].includes(s)) return false;
if (historyFilter.value === 'canceled' && s !== 'canceled') return false;
// Type (deposit = has order_id / credited_btx, withdrawal = type field)
if (historyTypeFilter.value === 'deposit' && item.type === 'withdrawal') return false;
if (historyTypeFilter.value === 'withdrawal' && item.type !== 'withdrawal') return false;
// Currency
if (historyCurrencyFilter.value !== 'all' && item.pay_currency !== historyCurrencyFilter.value) return false;
// Date from
if (historyDateFrom.value) {
const from = new Date(historyDateFrom.value).getTime();
if (new Date(item.created_at).getTime() < from) return false;
}
// Date to
if (historyDateTo.value) {
const to = new Date(historyDateTo.value).getTime() + 86400000;
if (new Date(item.created_at).getTime() > to) return false;
}
// Amount min (USD)
if (historyAmountMin.value) {
const amt = parseFloat(item.price_amount || item.pay_amount || '0');
if (amt < parseFloat(historyAmountMin.value)) return false;
}
// Amount max (USD)
if (historyAmountMax.value) {
const amt = parseFloat(item.price_amount || item.pay_amount || '0');
if (amt > parseFloat(historyAmountMax.value)) return false;
}
return true;
});
});
async function cancelDeposit(orderId: string) {
if (!confirm('Möchtest du diese ausstehende Einzahlung wirklich abbrechen?')) return;
try {
const resp = await csrfFetch(`/wallet/deposits/${orderId}`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Stornierung fehlgeschlagen');
await loadHistory();
} catch (e: any) {
alert(e.message || 'Ein Fehler ist aufgetreten.');
}
}
function minMaxFor(curr: string): { min: number; max: number } {
const s = currencies.value;
if (!s) return { min: 0, max: 0 };
const ov = s.overrides?.[curr] || {};
const min = typeof ov.min_usd === 'number' ? ov.min_usd : s.limits.global_min_usd;
const max = typeof ov.max_usd === 'number' ? ov.max_usd : s.limits.global_max_usd;
return { min, max };
}
async function startDeposit() {
if (!currencies.value) await loadCurrencies();
depositLoading.value = true;
depositError.value = null;
statusMsg.value = null;
startedOrder.value = null;
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
try {
const amt = parseFloat(amountInput.value || '0');
if (!isFinite(amt) || amt <= 0) throw new Error('Bitte einen gültigen Betrag eingeben');
const { min, max } = minMaxFor(selectedPayCurrency.value);
if (amt < min || amt > max) {
throw new Error(`Betrag außerhalb der Limits (${min} ${max} USD)`);
}
const resp = await csrfFetch('/wallet/deposits', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ currency: selectedPayCurrency.value, amount: amt }),
});
const j = await resp.json().catch(() => ({}));
if (!resp.ok) {
if (j?.message === 'amount_out_of_bounds') {
throw new Error(`Betrag außerhalb der Limits (${j.min_usd} ${j.max_usd})`);
}
throw new Error(j?.message || `HTTP ${resp.status}`);
}
startedOrder.value = {
order_id: j.order_id,
invoice_id: j.invoice_id,
pay_currency: j.pay_currency,
pay_address: j.pay_address || null,
redirect_url: j.redirect_url || null,
};
if (j.redirect_url) {
try { window.open(j.redirect_url, '_blank'); } catch {}
}
pollTimer = setInterval(async () => {
try {
const r = await csrfFetch(`/wallet/deposits/${j.order_id}`, { method: 'GET', headers: { 'Accept': 'application/json' } });
if (!r.ok) return;
const s = await r.json();
statusMsg.value = `Status: ${s.status || 'Warte auf Zahlung'}`;
if (s.credited_btx) {
statusMsg.value = `Erfolgreich! Eingezahlt: ${s.credited_btx} BTX`;
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
} catch {}
}, 5000);
} catch (e: any) {
depositError.value = e?.message || 'Einzahlung fehlgeschlagen';
} finally {
depositLoading.value = false;
}
}
const cancelOrder = () => {
startedOrder.value = null;
statusMsg.value = null;
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
};
const formatDate = (dateString: string) => {
if (!dateString) return '';
const d = new Date(dateString);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const formatStatus = (status: string) => {
const s = status.toLowerCase();
if (s === 'finished') return 'Erfolgreich';
if (s === 'waiting' || s === 'confirming' || s === 'new') return 'Ausstehend';
if (s === 'failed' || s === 'expired') return 'Fehlgeschlagen';
if (s === 'canceled') return 'Abgebrochen';
return status;
};
onMounted(() => {
loadCurrencies();
});
onUnmounted(() => {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
});
const getDisplayName = (cur: string) => {
return getCurrencyMeta(cur).name || cur;
};
</script>
<template>
<UserLayout>
<Head :title="$t('wallet.title')" />
<div class="wallet-page">
<!-- Page Header -->
<div class="page-header">
<div class="page-header-left">
<div class="page-header-icon">
<WalletMinimal :size="22" />
</div>
<div>
<h1>{{ $t('wallet.title') }}</h1>
<p>Verwalte deine Krypto-Assets</p>
</div>
</div>
<div class="page-header-balance" v-if="selectedCurrency">
<span class="bal-label">{{ selectedCurrency.name }}</span>
<div class="bal-value">
<span class="bal-amount">{{ selectedCurrency.amount.toFixed(4) }}</span>
<span class="bal-currency" :style="{ color: selectedCurrency.color }">
{{ selectedCurrency.currency === 'USDT_TRC20' ? 'USDT' : selectedCurrency.currency }}
</span>
</div>
</div>
</div>
<!-- Currency Selector Strip -->
<div class="coin-strip-wrapper">
<div class="coin-strip">
<button
v-for="coin in balances"
:key="coin.currency"
class="coin-chip"
:class="{ active: selectedCurrency?.currency === coin.currency }"
:style="selectedCurrency?.currency === coin.currency ? { '--accent': coin.color, borderColor: coin.color + '55', background: coin.color + '18' } : {}"
@click="selectCurrency(coin)"
>
<span class="chip-icon" :style="{ color: coin.color }">
<component :is="getIconComp(coin.icon)" :size="16" />
</span>
<span class="chip-ticker">{{ coin.currency === 'USDT_TRC20' ? 'USDT' : coin.currency }}</span>
<span class="chip-bal">{{ coin.amount.toFixed(3) }}</span>
</button>
</div>
</div>
<!-- Main Panel -->
<div class="main-panel" v-if="selectedCurrency">
<!-- Tabs -->
<div class="tab-bar">
<button class="tab-btn" :class="{ active: activeTab === 'deposit' }" @click="activeTab = 'deposit'">
<ArrowDownCircle :size="16" />
<span>Einzahlen</span>
</button>
<button class="tab-btn" :class="{ active: activeTab === 'withdraw' }" @click="activeTab = 'withdraw'">
<ArrowUpCircle :size="16" />
<span>Auszahlen</span>
</button>
<button class="tab-btn" :class="{ active: activeTab === 'history' }" @click="activeTab = 'history'">
<History :size="16" />
<span>Verlauf</span>
</button>
<button class="tab-btn" :class="{ active: activeTab === 'bets' }" @click="activeTab = 'bets'">
<Dices :size="16" />
<span>Einsätze</span>
</button>
</div>
<!-- Tab Content -->
<div class="tab-body">
<transition name="fade" mode="out-in">
<!-- DEPOSIT -->
<div v-if="activeTab === 'deposit'" key="deposit" class="tab-view">
<!-- Form -->
<div v-if="!startedOrder" class="form-view">
<div class="form-section">
<label class="field-label">Netzwerk / Zahlungsmethode</label>
<div class="select-row">
<span class="select-icon" :style="{ color: getCurrencyMeta(selectedPayCurrency).color }">
<component :is="getIconComp(getCurrencyMeta(selectedPayCurrency).icon)" :size="18" />
</span>
<select v-model="selectedPayCurrency" class="field-select">
<option v-for="c in (currencies?.enabled || [])" :key="c" :value="c">
{{ getDisplayName(c) }}
</option>
</select>
</div>
</div>
<div class="form-section">
<label class="field-label">Einzahlungsbetrag (USD)</label>
<div class="amount-field">
<span class="amount-prefix">$</span>
<input type="number" v-model="amountInput" placeholder="0" min="0" step="1" class="amount-input" />
<span class="amount-suffix">USD</span>
</div>
<div class="limits-row" v-if="currencies">
<span class="limit-pill">Min ${{ minMaxFor(selectedPayCurrency).min }}</span>
<span class="limit-pill">Max ${{ minMaxFor(selectedPayCurrency).max }}</span>
</div>
</div>
<div class="receive-preview" v-if="currencies">
<span class="receive-label">Du erhältst ca.</span>
<span class="receive-value">
{{ (parseFloat(amountInput || '0') * (currencies?.btx_per_usd || 1)).toFixed(2) }} BTX
</span>
</div>
<button class="btn-deposit" :disabled="depositLoading" @click="startDeposit">
<Loader2 v-if="depositLoading" :size="18" class="spin" />
<CreditCard v-else :size="18" />
{{ depositLoading ? 'Wird vorbereitet...' : 'Jetzt Einzahlen' }}
</button>
<div class="info-box" v-if="!depositError">
<Info :size="15" />
<span>Sicher abgewickelt über <b>NOWPayments</b></span>
</div>
<div class="error-box" v-if="depositError">
<AlertCircle :size="15" />
<span>{{ depositError }}</span>
</div>
</div>
<!-- Order Box -->
<div v-else class="order-view">
<div class="order-topbar">
<h4>Zahlung abschließen</h4>
<button class="btn-ghost" @click="cancelOrder">Abbrechen</button>
</div>
<p class="order-hint">Sende den Betrag an die folgende Adresse oder öffne den NOWPayments Checkout.</p>
<div class="addr-field" v-if="startedOrder?.pay_address">
<input type="text" :value="startedOrder.pay_address" readonly />
<button class="btn-copy" @click="copyToClipboard(startedOrder!.pay_address!)">
<Copy :size="16" />
</button>
</div>
<div class="qr-placeholder" v-if="startedOrder?.pay_address">
<QrCode :size="120" />
</div>
<a v-if="startedOrder?.redirect_url" class="btn-checkout" :href="startedOrder.redirect_url" target="_blank" rel="noopener">
<ExternalLink :size="16" />
NOWPayments Checkout öffnen
</a>
<div class="status-pill" :class="{ success: statusMsg?.includes('Erfolgreich') }">
<CheckCircle v-if="statusMsg?.includes('Erfolgreich')" :size="16" />
<Loader2 v-else :size="16" class="spin" />
{{ statusMsg || 'Warte auf eingehende Transaktion...' }}
</div>
<div class="order-id-line">Order: <code>{{ startedOrder?.order_id }}</code></div>
<div class="warning-box">
<AlertTriangle :size="15" />
<span>Sende ausschließlich <strong>{{ getDisplayName(startedOrder?.pay_currency || '') }}</strong> über das exakt übereinstimmende Netzwerk. Andere Coins gehen unwiderruflich verloren.</span>
</div>
</div>
</div>
<!-- WITHDRAW -->
<div v-else-if="activeTab === 'withdraw'" key="withdraw" class="tab-view">
<div class="form-view">
<div class="form-section">
<label class="field-label">Auszahlungsnetzwerk</label>
<div class="network-display">
<span :style="{ color: selectedCurrency.color }">
<component :is="getIconComp(selectedCurrency.icon)" :size="18" />
</span>
<span>{{ selectedCurrency.name }} ({{ selectedCurrency.currency === 'USDT_TRC20' ? 'USDT TRC20' : selectedCurrency.currency }})</span>
</div>
</div>
<div class="form-section">
<label class="field-label">Empfängeradresse</label>
<div class="icon-field">
<WalletMinimal :size="16" class="icon-field-icon" />
<input type="text" placeholder="Deine Wallet-Adresse einfügen..." class="field-input" />
</div>
</div>
<div class="form-section">
<label class="field-label">Auszahlungsbetrag</label>
<div class="amount-field">
<input type="number" placeholder="0.00" class="amount-input" />
<span class="amount-suffix">BTX</span>
<button class="btn-max">MAX</button>
</div>
<p class="field-hint">Verfügbar: <b>{{ balances.find(b => b.currency === 'BTX')?.amount.toFixed(2) || '0.00' }} BTX</b></p>
</div>
<button class="btn-withdraw">
Auszahlung beantragen <ArrowRight :size="16" />
</button>
<div class="info-box">
<Info :size="15" />
<span>Auszahlungen werden manuell geprüft und dauern bis zu 24 Stunden.</span>
</div>
</div>
</div>
<!-- HISTORY -->
<div v-else-if="activeTab === 'history'" key="history" class="tab-view">
<!-- Filter Toolbar -->
<div class="hist-toolbar">
<div class="hist-status-pills">
<button class="filter-pill" :class="{ active: historyFilter === 'all' }" @click="historyFilter = 'all'">Alle</button>
<button class="filter-pill success" :class="{ active: historyFilter === 'finished' }" @click="historyFilter = 'finished'">Erfolgreich</button>
<button class="filter-pill warning" :class="{ active: historyFilter === 'pending' }" @click="historyFilter = 'pending'">Ausstehend</button>
<button class="filter-pill danger" :class="{ active: historyFilter === 'canceled' }" @click="historyFilter = 'canceled'">Abgebrochen</button>
</div>
<div class="hist-actions">
<button v-if="activeFilterCount > 0" class="btn-reset-filters" @click="resetHistoryFilters">
<RotateCcw :size="13" /> Zurücksetzen
<span class="filter-count">{{ activeFilterCount }}</span>
</button>
<button class="btn-filter-toggle" :class="{ active: historyShowFilters }" @click="historyShowFilters = !historyShowFilters">
<SlidersHorizontal :size="15" />
Filter
</button>
</div>
</div>
<!-- Advanced Filters Panel -->
<transition name="filter-slide">
<div v-if="historyShowFilters" class="filter-panel">
<div class="filter-grid">
<!-- Type -->
<div class="filter-field">
<label>Typ</label>
<select v-model="historyTypeFilter" class="filter-select">
<option value="all">Alle Typen</option>
<option value="deposit">Einzahlung</option>
<option value="withdrawal">Auszahlung</option>
</select>
</div>
<!-- Currency -->
<div class="filter-field">
<label>Währung</label>
<select v-model="historyCurrencyFilter" class="filter-select">
<option value="all">Alle Währungen</option>
<option v-for="c in historyAvailableCurrencies" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<!-- Date From -->
<div class="filter-field">
<label>Datum von</label>
<input type="date" v-model="historyDateFrom" class="filter-input" />
</div>
<!-- Date To -->
<div class="filter-field">
<label>Datum bis</label>
<input type="date" v-model="historyDateTo" class="filter-input" />
</div>
<!-- Amount Min -->
<div class="filter-field">
<label>Betrag min ($)</label>
<input type="number" v-model="historyAmountMin" placeholder="0" class="filter-input" min="0" />
</div>
<!-- Amount Max -->
<div class="filter-field">
<label>Betrag max ($)</label>
<input type="number" v-model="historyAmountMax" placeholder="∞" class="filter-input" min="0" />
</div>
</div>
</div>
</transition>
<!-- Results count -->
<div class="hist-results-bar" v-if="!historyLoading && paymentHistory.length > 0">
<span>{{ filteredHistory.length }} von {{ paymentHistory.length }} Einträgen</span>
</div>
<div v-if="historyLoading" class="empty-state">
<Loader2 :size="28" class="spin" />
<p>Lade Verlauf...</p>
</div>
<div v-else-if="filteredHistory.length === 0" class="empty-state">
<Clock :size="32" class="empty-icon-svg" />
<p>Keine Transaktionen gefunden.</p>
<button v-if="activeFilterCount > 0" class="btn-reset-filters" @click="resetHistoryFilters">
<RotateCcw :size="13" /> Filter zurücksetzen
</button>
</div>
<div v-else class="tx-list">
<div v-for="item in filteredHistory" :key="item.order_id" class="tx-item" :class="item.status">
<div class="tx-icon" :class="item.status">
<ArrowUpToLine v-if="item.type === 'withdrawal'" :size="16" />
<ArrowDownToLine v-else :size="16" />
</div>
<div class="tx-info">
<div class="tx-title">
{{ item.type === 'withdrawal' ? 'Auszahlung' : 'Einzahlung' }}
<span class="tx-currency-tag">{{ item.pay_currency }}</span>
</div>
<div class="tx-date">{{ formatDate(item.created_at) }}</div>
</div>
<div class="tx-amount">
<div class="tx-btx" v-if="item.credited_btx">+{{ item.credited_btx }} BTX</div>
<div class="tx-sub" v-if="item.pay_amount">{{ item.pay_amount }} {{ item.pay_currency }}</div>
<div class="tx-sub" v-else-if="item.price_amount">~${{ item.price_amount }}</div>
</div>
<div class="tx-status-col">
<span class="tx-badge" :class="item.status">{{ formatStatus(item.status) }}</span>
<button v-if="['waiting','new','confirming'].includes(item.status)" class="btn-cancel-tx" @click="cancelDeposit(item.order_id)">Abbrechen</button>
</div>
</div>
</div>
</div>
<!-- BETS -->
<div v-else-if="activeTab === 'bets'" key="bets" class="tab-view">
<div class="bets-toolbar">
<div class="search-field">
<Search :size="15" class="search-icon" />
<input type="text" v-model="betsSearch" placeholder="Spiel suchen..." />
</div>
</div>
<div v-if="betsLoading && betsHistory.length === 0" class="empty-state">
<Loader2 :size="28" class="spin" />
<p>Lade Einsätze...</p>
</div>
<div v-else-if="betsHistory.length === 0" class="empty-state">
<Dices :size="32" class="empty-icon-svg" />
<p>Keine Einsätze gefunden.</p>
</div>
<div v-else class="bets-wrap">
<table class="bets-table">
<thead>
<tr>
<th @click="toggleBetsSort('game_name')" class="sortable">Spiel <ArrowUpDown :size="12" /></th>
<th @click="toggleBetsSort('created_at')" class="sortable">Zeit <ArrowUpDown :size="12" /></th>
<th @click="toggleBetsSort('wager_amount')" class="sortable text-right">Einsatz <ArrowUpDown :size="12" /></th>
<th @click="toggleBetsSort('payout_multiplier')" class="sortable text-right">Multi <ArrowUpDown :size="12" /></th>
<th @click="toggleBetsSort('payout_amount')" class="sortable text-right">Auszahlung <ArrowUpDown :size="12" /></th>
</tr>
</thead>
<tbody>
<tr v-for="bet in betsHistory" :key="bet.id">
<td class="b-game">{{ bet.game_name }}</td>
<td class="b-time">{{ formatDate(bet.created_at) }}</td>
<td class="text-right b-mono">{{ parseFloat(bet.wager_amount).toFixed(2) }} {{ bet.currency }}</td>
<td class="text-right b-mult">{{ parseFloat(bet.payout_multiplier).toFixed(2) }}x</td>
<td class="text-right b-mono" :class="parseFloat(bet.payout_amount) > parseFloat(bet.wager_amount) ? 'win' : 'loss'">
{{ parseFloat(bet.payout_amount).toFixed(2) }} {{ bet.currency }}
</td>
</tr>
</tbody>
</table>
<div class="pagination" v-if="betsTotalPages > 1">
<button class="page-btn" :disabled="betsPage === 1" @click="betsPage--">
<ChevronLeft :size="16" />
</button>
<span class="page-info">{{ betsPage }} / {{ betsTotalPages }}</span>
<button class="page-btn" :disabled="betsPage === betsTotalPages" @click="betsPage++">
<ChevronRight :size="16" />
</button>
</div>
</div>
</div>
</transition>
</div>
</div>
<!-- Vault Banner -->
<div class="vault-banner" @click="showVault = true">
<div class="vault-left">
<div class="vault-icon">
<ShieldCheck :size="24" />
</div>
<div>
<h3>Dein Vault</h3>
<p>Sichere deine Gewinne sofort verfügbar.</p>
</div>
</div>
<button class="vault-btn">
Vault öffnen <ChevronRight :size="16" />
</button>
</div>
</div>
<VaultModal :open="showVault" :coins="balances" @close="showVault = false" />
</UserLayout>
</template>
<style scoped>
/* ── Base ── */
.wallet-page {
max-width: 900px;
margin: 0 auto;
padding: 28px 16px 48px;
display: flex;
flex-direction: column;
gap: 16px;
animation: page-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes page-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Page Header ── */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
animation: page-in 0.6s 0.05s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.page-header-left {
display: flex;
align-items: center;
gap: 14px;
}
.page-header-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: linear-gradient(135deg, #1c1c20, #111113);
border: 1px solid #2a2a2f;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s;
}
.page-header-icon:hover {
transform: rotate(-8deg) scale(1.08);
box-shadow: 0 0 18px rgba(255,0,122,0.25);
}
.page-header-left h1 {
font-size: 18px;
font-weight: 800;
color: #fff;
margin: 0;
letter-spacing: -0.3px;
}
.page-header-left p {
font-size: 12px;
color: #71717a;
margin: 2px 0 0;
}
.page-header-balance {
text-align: right;
}
.bal-label {
display: block;
font-size: 11px;
font-weight: 700;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 3px;
}
.bal-value {
display: flex;
align-items: baseline;
justify-content: flex-end;
gap: 5px;
}
.bal-amount {
font-size: 26px;
font-weight: 800;
color: #fff;
letter-spacing: -1px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.bal-currency {
font-size: 13px;
font-weight: 700;
transition: color 0.3s;
}
/* ── Coin Strip ── */
.coin-strip-wrapper {
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
margin: 0 -16px;
padding: 4px 16px;
animation: page-in 0.6s 0.1s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.coin-strip-wrapper::-webkit-scrollbar { display: none; }
.coin-strip {
display: flex;
gap: 8px;
width: max-content;
}
.coin-chip {
display: flex;
align-items: center;
gap: 7px;
padding: 8px 13px;
border-radius: 10px;
border: 1px solid #27272a;
background: #111113;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
border-color 0.2s,
background 0.2s,
box-shadow 0.2s;
white-space: nowrap;
position: relative;
overflow: hidden;
}
.coin-chip::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.coin-chip:hover {
border-color: #3f3f46;
background: #18181b;
transform: translateY(-2px) scale(1.02);
box-shadow: 0 6px 16px rgba(0,0,0,0.4);
}
.coin-chip:hover::after { opacity: 1; }
.coin-chip:active {
transform: translateY(0) scale(0.97);
transition-duration: 0.08s;
}
.coin-chip.active {
border-color: var(--accent, #ff007a);
box-shadow: 0 0 0 1px var(--accent, #ff007a), 0 4px 20px -4px color-mix(in srgb, var(--accent, #ff007a) 40%, transparent);
transform: translateY(-1px);
}
.chip-icon {
display: flex;
align-items: center;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.coin-chip:hover .chip-icon {
transform: scale(1.2) rotate(-5deg);
}
.chip-ticker {
font-size: 12px;
font-weight: 700;
color: #e4e4e7;
}
.chip-bal {
font-size: 11px;
color: #52525b;
font-weight: 600;
font-variant-numeric: tabular-nums;
transition: color 0.2s;
}
.coin-chip:hover .chip-bal,
.coin-chip.active .chip-bal {
color: #a1a1aa;
}
/* ── Main Panel ── */
.main-panel {
background: #0d0d0f;
border: 1px solid #1f1f23;
border-radius: 20px;
overflow: hidden;
animation: page-in 0.6s 0.15s cubic-bezier(0.16, 1, 0.3, 1) both;
transition: box-shadow 0.3s;
}
/* ── Tab Bar ── */
.tab-bar {
display: flex;
gap: 4px;
padding: 12px 16px;
border-bottom: 1px solid #1f1f23;
background: #0a0a0c;
overflow-x: auto;
scrollbar-width: none;
position: relative;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-btn {
display: flex;
align-items: center;
gap: 7px;
padding: 8px 16px;
border-radius: 8px;
border: none;
background: transparent;
color: #52525b;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s, background 0.2s, transform 0.15s;
white-space: nowrap;
flex-shrink: 0;
position: relative;
}
.tab-btn::after {
content: '';
position: absolute;
bottom: -13px;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 70%;
height: 2px;
background: #ff007a;
border-radius: 2px 2px 0 0;
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tab-btn:hover {
color: #d4d4d8;
background: #18181b;
transform: translateY(-1px);
}
.tab-btn:active { transform: translateY(0); }
.tab-btn.active {
color: #fff;
background: #1c1c1f;
}
.tab-btn.active::after {
transform: translateX(-50%) scaleX(1);
}
.tab-btn svg {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tab-btn:hover svg,
.tab-btn.active svg {
transform: scale(1.2);
}
/* ── Tab Body ── */
.tab-body {
padding: 24px 20px;
}
@media (min-width: 600px) {
.tab-body { padding: 28px 32px; }
}
/* ── Form View ── */
.form-view {
max-width: 480px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-label {
font-size: 11px;
font-weight: 700;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.6px;
}
.select-row {
display: flex;
align-items: center;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.select-row:focus-within {
border-color: #ff007a;
box-shadow: 0 0 0 3px rgba(255,0,122,0.1);
}
.select-icon {
padding: 0 14px;
display: flex;
align-items: center;
border-right: 1px solid #27272a;
height: 48px;
flex-shrink: 0;
}
.field-select {
flex: 1;
background: transparent;
border: none;
color: #fff;
padding: 14px 16px;
font-size: 14px;
font-weight: 500;
outline: none;
cursor: pointer;
appearance: none;
}
.amount-field {
display: flex;
align-items: center;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.amount-field:focus-within {
border-color: #ff007a;
box-shadow: 0 0 0 3px rgba(255,0,122,0.1);
}
.amount-prefix {
padding-left: 16px;
font-size: 16px;
font-weight: 600;
color: #52525b;
}
.amount-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
padding: 14px 10px;
font-size: 20px;
font-weight: 700;
font-family: monospace;
outline: none;
}
.amount-suffix {
padding-right: 14px;
font-size: 12px;
font-weight: 700;
color: #52525b;
}
.btn-max {
background: #27272a;
color: #fff;
border: none;
font-size: 10px;
font-weight: 800;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
margin-right: 8px;
transition: 0.15s;
}
.btn-max:hover { background: #3f3f46; }
.limits-row {
display: flex;
gap: 8px;
}
.limit-pill {
background: #111113;
border: 1px solid #27272a;
border-radius: 6px;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
color: #71717a;
}
.receive-preview {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(59,130,246,0.06);
border: 1px solid rgba(59,130,246,0.15);
border-radius: 12px;
padding: 14px 16px;
}
.receive-label { font-size: 13px; color: #93c5fd; font-weight: 600; }
.receive-value { font-size: 18px; font-weight: 800; color: #fff; }
.btn-deposit {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
background: linear-gradient(135deg, #ff007a, #d6005e);
box-shadow: 0 8px 24px -6px rgba(255,0,122,0.45);
color: #fff;
border: none;
padding: 15px;
border-radius: 12px;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.2s,
filter 0.2s;
position: relative;
overflow: hidden;
}
.btn-deposit::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.12) 50%, transparent 100%);
transform: translateX(-100%);
transition: transform 0.5s;
}
.btn-deposit:hover:not(:disabled) {
transform: translateY(-2px) scale(1.01);
box-shadow: 0 14px 32px -6px rgba(255,0,122,0.55);
}
.btn-deposit:hover:not(:disabled)::before {
transform: translateX(100%);
}
.btn-deposit:active:not(:disabled) {
transform: translateY(0) scale(0.98);
transition-duration: 0.08s;
}
.btn-deposit:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-withdraw {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
background: #1c1c1f;
border: 1px solid #2a2a2f;
color: #fff;
padding: 15px;
border-radius: 12px;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.btn-withdraw:hover {
background: #27272a;
border-color: #3f3f46;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.info-box {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(59,130,246,0.07);
border: 1px solid rgba(59,130,246,0.15);
border-radius: 10px;
padding: 11px 14px;
font-size: 12px;
color: #60a5fa;
line-height: 1.5;
}
.info-box b { color: #93c5fd; }
.error-box {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.2);
border-radius: 10px;
padding: 11px 14px;
font-size: 12px;
color: #fca5a5;
}
.field-hint {
font-size: 12px;
color: #52525b;
text-align: right;
margin: 0;
}
.field-hint b { color: #a1a1aa; }
.network-display {
display: flex;
align-items: center;
gap: 12px;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
padding: 14px 16px;
font-size: 14px;
font-weight: 600;
color: #fff;
}
.icon-field {
position: relative;
}
.icon-field-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #52525b;
}
.field-input {
width: 100%;
background: #111113;
border: 1px solid #27272a;
color: #fff;
padding: 14px 14px 14px 42px;
border-radius: 12px;
font-size: 14px;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.field-input:focus {
border-color: #ff007a;
box-shadow: 0 0 0 3px rgba(255,0,122,0.1);
}
/* ── Order View ── */
.order-view {
max-width: 480px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.order-topbar {
display: flex;
align-items: center;
justify-content: space-between;
}
.order-topbar h4 {
font-size: 16px;
font-weight: 700;
color: #fff;
margin: 0;
}
.btn-ghost {
background: transparent;
border: 1px solid #27272a;
color: #71717a;
font-size: 12px;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
transition: 0.15s;
}
.btn-ghost:hover { background: #18181b; color: #fff; }
.order-hint {
font-size: 13px;
color: #71717a;
margin: 0;
line-height: 1.6;
}
.addr-field {
display: flex;
gap: 8px;
}
.addr-field input {
flex: 1;
background: #111113;
border: 1px solid #27272a;
color: #10b981;
padding: 12px 14px;
border-radius: 10px;
font-family: monospace;
font-size: 12px;
text-align: center;
outline: none;
}
.qr-placeholder {
display: flex;
justify-content: center;
padding: 20px;
background: #fff;
border-radius: 14px;
color: #000;
}
.status-pill {
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
background: rgba(245,158,11,0.08);
border: 1px solid rgba(245,158,11,0.2);
color: #f59e0b;
padding: 11px 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
transition: all 0.3s;
}
.status-pill.success {
background: rgba(16,185,129,0.08);
border-color: rgba(16,185,129,0.2);
color: #10b981;
}
.order-id-line {
font-size: 12px;
color: #52525b;
text-align: center;
}
.order-id-line code { color: #a1a1aa; font-family: monospace; }
.warning-box {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(239,68,68,0.07);
border: 1px solid rgba(239,68,68,0.18);
border-radius: 10px;
padding: 12px 14px;
font-size: 12px;
color: #fca5a5;
line-height: 1.5;
}
.warning-box strong { color: #ef4444; }
/* ── History ── */
.filter-row {
display: flex;
gap: 6px;
margin-bottom: 20px;
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 4px;
}
.filter-row::-webkit-scrollbar { display: none; }
.filter-pill {
background: #111113;
border: 1px solid #27272a;
color: #52525b;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: color 0.18s, border-color 0.18s, background 0.18s,
transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.18s;
white-space: nowrap;
flex-shrink: 0;
}
.filter-pill:hover {
color: #d4d4d8;
border-color: #3f3f46;
transform: translateY(-1px);
}
.filter-pill:active { transform: scale(0.95); }
.filter-pill.active {
background: linear-gradient(135deg, #ff007a22, #ff007a11);
color: #ff007a;
border-color: #ff007a55;
box-shadow: 0 0 12px -4px rgba(255,0,122,0.3);
}
.tx-list { display: flex; flex-direction: column; gap: 8px; }
.tx-item {
display: flex;
align-items: center;
gap: 12px;
background: #111113;
border: 1px solid #1f1f23;
border-left: 3px solid transparent;
border-radius: 12px;
padding: 14px 16px;
transition: border-color 0.2s, background 0.2s,
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.2s;
animation: item-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.tx-item:nth-child(1) { animation-delay: 0.03s; }
.tx-item:nth-child(2) { animation-delay: 0.06s; }
.tx-item:nth-child(3) { animation-delay: 0.09s; }
.tx-item:nth-child(4) { animation-delay: 0.12s; }
.tx-item:nth-child(5) { animation-delay: 0.15s; }
@keyframes item-in {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
.tx-item:hover {
background: #141416;
border-color: #2a2a2f;
border-left-color: #ff007a;
transform: translateX(3px);
box-shadow: -4px 0 16px -4px rgba(255,0,122,0.2);
}
.tx-item.finished:hover { border-left-color: #10b981; box-shadow: -4px 0 16px -4px rgba(16,185,129,0.2); }
.tx-item.failed:hover, .tx-item.expired:hover, .tx-item.canceled:hover { border-left-color: #ef4444; box-shadow: -4px 0 16px -4px rgba(239,68,68,0.2); }
.tx-item.waiting:hover, .tx-item.confirming:hover { border-left-color: #f59e0b; box-shadow: -4px 0 16px -4px rgba(245,158,11,0.2); }
.tx-icon {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #1a1a1e;
color: #52525b;
}
.tx-icon.finished { background: rgba(16,185,129,0.1); color: #10b981; }
.tx-icon.waiting, .tx-icon.confirming, .tx-icon.new { background: rgba(245,158,11,0.1); color: #f59e0b; }
.tx-icon.failed, .tx-icon.expired, .tx-icon.canceled { background: rgba(239,68,68,0.1); color: #ef4444; }
.tx-info { flex: 1; min-width: 0; }
.tx-title { font-size: 13px; font-weight: 700; color: #e4e4e7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tx-date { font-size: 11px; color: #52525b; margin-top: 2px; }
.tx-amount { text-align: right; flex-shrink: 0; }
.tx-btx { font-size: 14px; font-weight: 800; color: #fff; }
.tx-sub { font-size: 11px; color: #52525b; margin-top: 2px; }
.tx-status-col { text-align: right; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; min-width: 80px; }
.tx-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 3px 8px;
border-radius: 5px;
background: #1a1a1e;
color: #52525b;
}
.tx-badge.finished { background: rgba(16,185,129,0.1); color: #10b981; }
.tx-badge.waiting, .tx-badge.confirming, .tx-badge.new { background: rgba(245,158,11,0.1); color: #f59e0b; }
.tx-badge.failed, .tx-badge.expired, .tx-badge.canceled { background: rgba(239,68,68,0.1); color: #ef4444; }
.btn-cancel-tx {
background: none;
border: none;
color: #ef4444;
font-size: 11px;
font-weight: 600;
cursor: pointer;
padding: 0;
text-decoration: underline;
}
/* ── Bets ── */
.bets-toolbar { margin-bottom: 16px; }
.search-field {
position: relative;
max-width: 280px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #52525b;
}
.search-field input {
width: 100%;
background: #111113;
border: 1px solid #27272a;
border-radius: 9px;
padding: 9px 12px 9px 34px;
color: #fff;
font-size: 13px;
outline: none;
box-sizing: border-box;
transition: border-color 0.15s;
}
.search-field input:focus { border-color: #3f3f46; }
.bets-wrap { overflow-x: auto; border: 1px solid #1f1f23; border-radius: 14px; background: #0a0a0c; }
.bets-table { width: 100%; border-collapse: collapse; min-width: 500px; }
.bets-table th {
padding: 12px 16px;
text-align: left;
font-size: 11px;
font-weight: 700;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid #1f1f23;
white-space: nowrap;
}
.bets-table th.sortable { cursor: pointer; user-select: none; }
.bets-table th.sortable:hover { color: #a1a1aa; }
.bets-table th svg { display: inline; vertical-align: middle; margin-left: 4px; opacity: 0.5; }
.bets-table td { padding: 13px 16px; font-size: 13px; border-bottom: 1px solid #111113; color: #d4d4d8; }
.bets-table tbody tr {
transition: background 0.15s, transform 0.15s;
}
.bets-table tbody tr:hover {
background: #131316;
}
.bets-table tbody tr:last-child td { border-bottom: none; }
.b-game { font-weight: 600; color: #fff; }
.b-time { font-size: 11px; color: #52525b; }
.b-mono { font-family: monospace; }
.b-mult { font-weight: 700; }
.text-right { text-align: right; }
.win { color: #10b981; font-weight: 700; }
.loss { color: #52525b; }
/* ── Pagination ── */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
padding: 14px;
border-top: 1px solid #1f1f23;
}
.page-btn {
background: #18181b;
border: 1px solid #27272a;
color: #fff;
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s, border-color 0.15s,
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.page-btn:hover:not(:disabled) {
background: #27272a;
border-color: #3f3f46;
transform: scale(1.1);
}
.page-btn:active:not(:disabled) { transform: scale(0.92); }
.page-info { font-size: 13px; font-weight: 600; color: #52525b; }
/* ── Empty State ── */
.empty-state {
text-align: center;
padding: 56px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
color: #52525b;
}
.empty-icon-svg { opacity: 0.3; }
.empty-state p { font-size: 14px; margin: 0; }
/* ── Vault Banner ── */
.vault-banner {
background: #0d0d0f;
border: 1px solid #1f1f23;
border-radius: 16px;
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
transition: border-color 0.25s, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s;
position: relative;
overflow: hidden;
animation: page-in 0.6s 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.vault-banner::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(to bottom, #ff007a, #ff4da6);
border-radius: 0 2px 2px 0;
transition: width 0.3s;
}
.vault-banner::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(255,0,122,0.04) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.vault-banner:hover {
border-color: rgba(255,0,122,0.3);
transform: translateY(-3px);
box-shadow: 0 16px 40px -10px rgba(255,0,122,0.2), 0 8px 24px rgba(0,0,0,0.4);
}
.vault-banner:hover::before { width: 4px; }
.vault-banner:hover::after { opacity: 1; }
.vault-banner:active { transform: translateY(-1px); transition-duration: 0.1s; }
.vault-left { display: flex; align-items: center; gap: 16px; }
.vault-icon {
width: 48px;
height: 48px;
background: rgba(255,0,122,0.08);
border: 1px solid rgba(255,0,122,0.2);
border-radius: 13px;
display: flex;
align-items: center;
justify-content: center;
color: #ff007a;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s;
}
.vault-banner:hover .vault-icon {
transform: scale(1.12) rotate(-5deg);
box-shadow: 0 0 20px rgba(255,0,122,0.3);
}
.vault-left h3 { font-size: 15px; font-weight: 800; color: #fff; margin: 0 0 3px; }
.vault-left p { font-size: 12px; color: #52525b; margin: 0; transition: color 0.2s; }
.vault-banner:hover .vault-left p { color: #71717a; }
.vault-btn {
display: flex;
align-items: center;
gap: 6px;
background: #fff;
color: #000;
border: none;
padding: 9px 18px;
border-radius: 9px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
white-space: nowrap;
flex-shrink: 0;
}
.vault-btn:hover { background: #e4e4e7; transform: scale(1.04); }
.vault-btn svg { transition: transform 0.2s; }
.vault-banner:hover .vault-btn svg { transform: translateX(3px); }
/* ── Spinner ── */
.spin { animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Copy Button ── */
.btn-copy {
background: #18181b;
border: 1px solid #27272a;
color: #a1a1aa;
width: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s, color 0.15s,
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
flex-shrink: 0;
}
.btn-copy:hover {
background: #27272a;
color: #10b981;
transform: scale(1.08);
}
.btn-copy:active { transform: scale(0.92); }
/* ── Checkout Button ── */
.btn-checkout {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-weight: 700;
font-size: 13px;
text-decoration: none;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s;
}
.btn-checkout:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px -4px rgba(37,99,235,0.4);
}
/* ── Transitions ── */
.fade-enter-active {
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.fade-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.fade-enter-from {
opacity: 0;
transform: translateY(6px);
}
.fade-leave-to {
opacity: 0;
transform: translateY(-4px);
}
/* ── Responsive ── */
/* ── History Filter System ── */
.hist-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.hist-status-pills {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.hist-actions {
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.btn-filter-toggle {
display: flex;
align-items: center;
gap: 7px;
background: #111113;
border: 1px solid #27272a;
color: #a1a1aa;
padding: 7px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-filter-toggle:hover {
background: #18181b;
border-color: #3f3f46;
color: #fff;
}
.btn-filter-toggle.active {
background: rgba(255,0,122,0.1);
border-color: rgba(255,0,122,0.4);
color: #ff007a;
}
.btn-reset-filters {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
border: 1px solid rgba(239,68,68,0.3);
color: #ef4444;
padding: 7px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-reset-filters:hover {
background: rgba(239,68,68,0.1);
border-color: rgba(239,68,68,0.5);
}
.filter-count {
background: #ff007a;
color: #fff;
font-size: 10px;
font-weight: 800;
width: 18px;
height: 18px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Filter Panel */
.filter-panel {
background: #0a0a0c;
border: 1px solid #1f1f23;
border-radius: 14px;
padding: 18px;
margin-bottom: 16px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 14px;
}
.filter-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-field label {
font-size: 11px;
font-weight: 700;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-select,
.filter-input {
background: #111113;
border: 1px solid #27272a;
color: #e4e4e7;
padding: 9px 12px;
border-radius: 9px;
font-size: 13px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
width: 100%;
box-sizing: border-box;
}
.filter-select:focus,
.filter-input:focus {
border-color: #ff007a;
box-shadow: 0 0 0 3px rgba(255,0,122,0.1);
}
.filter-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(0.5);
cursor: pointer;
}
/* Filter slide transition */
.filter-slide-enter-active {
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.filter-slide-leave-active {
transition: all 0.18s ease;
}
.filter-slide-enter-from,
.filter-slide-leave-to {
opacity: 0;
transform: translateY(-8px);
}
/* Results bar */
.hist-results-bar {
font-size: 12px;
color: #52525b;
margin-bottom: 12px;
padding: 0 2px;
}
/* Colored filter pills */
.filter-pill.success.active {
background: rgba(16,185,129,0.12);
color: #10b981;
border-color: rgba(16,185,129,0.4);
box-shadow: 0 0 12px -4px rgba(16,185,129,0.3);
}
.filter-pill.warning.active {
background: rgba(245,158,11,0.12);
color: #f59e0b;
border-color: rgba(245,158,11,0.4);
box-shadow: 0 0 12px -4px rgba(245,158,11,0.3);
}
.filter-pill.danger.active {
background: rgba(239,68,68,0.12);
color: #ef4444;
border-color: rgba(239,68,68,0.4);
box-shadow: 0 0 12px -4px rgba(239,68,68,0.3);
}
/* Currency tag in tx title */
.tx-currency-tag {
display: inline-block;
background: #1f1f23;
border: 1px solid #27272a;
border-radius: 4px;
padding: 1px 6px;
font-size: 10px;
font-weight: 700;
color: #71717a;
margin-left: 6px;
vertical-align: middle;
}
@media (max-width: 600px) {
.wallet-page { padding: 16px 12px 40px; gap: 12px; }
.page-header { gap: 12px; }
.bal-amount { font-size: 22px; }
.vault-banner { flex-direction: column; align-items: flex-start; }
.vault-btn { width: 100%; justify-content: center; }
.tx-item { flex-wrap: wrap; }
.tx-status-col { flex-direction: row; align-items: center; gap: 8px; min-width: unset; }
.tab-body { padding: 16px 14px; }
.hist-toolbar { gap: 8px; }
.filter-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 400px) {
.page-header { flex-direction: column; align-items: flex-start; }
.page-header-balance { text-align: left; }
.bal-value { justify-content: flex-start; }
.filter-grid { grid-template-columns: 1fr; }
}
</style>