2159 lines
70 KiB
Vue
2159 lines
70 KiB
Vue
<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>
|