Files
BetiX/resources/js/layouts/user/userlayout.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

1508 lines
72 KiB
Vue

<script setup lang="ts">
import { usePage, Link, router } from '@inertiajs/vue3';
import { ref, onMounted, nextTick, computed, watch, onUnmounted } from 'vue';
import { useNotifications } from '@/composables/useNotifications';
import { usePrimaryColor } from '@/composables/usePrimaryColor';
import { setLocale as setI18nLocale } from '@/i18n';
import { initializeApiUrl } from '@/utils/api'; // Import helper
import { csrfFetch } from '@/utils/csrfFetch';
import AuthModals from '../../components/auth/AuthModals.vue';
import GlobalChat from '../../components/chat/GlobalChat.vue';
import SupportChat from '../../components/support/SupportChat.vue';
import AppLoading from '../../components/ui/AppLoading.vue';
import Footer from '../../components/ui/Footer.vue';
import SearchModal from '../../components/ui/SearchModal.vue';
import Notification from '../../components/ui/Notification.vue';
import VaultModal from '../../components/vault/VaultModal.vue';
const page = usePage();
const user = computed(() => page.props.auth.user);
const stats = computed(() => user.value?.stats || {});
// Initialize API URL globally
onMounted(() => {
const apiUrlFromProps = (page.props as any).api_url;
initializeApiUrl(apiUrlFromProps);
if (!(window as any).lucide) {
const script = document.createElement('script');
script.src = "https://unpkg.com/lucide@latest";
script.onload = () => { (window as any).lucide.createIcons(); };
document.head.appendChild(script);
} else {
(window as any).lucide.createIcons();
}
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleKeydown);
document.addEventListener('keydown', handleGlobalKeydown);
// Listen for events from GamePlay to control sidebar state
document.addEventListener('collapse-sidebar', forceCollapseSidebar);
document.addEventListener('expand-sidebar', forceExpandSidebar);
});
// --- VIP Logic ---
const vipLevelsConfig = [
{ name: 'Newbie', color: '#888888', class: 'rank-newbie' },
{ name: 'Bronze', color: '#cd7f32', class: 'rank-bronze' },
{ name: 'Silver', color: '#c0c0c0', class: 'rank-silver' },
{ name: 'Gold', color: '#ffd700', class: 'rank-gold' },
{ name: 'Platinum', color: '#00f2ff', class: 'rank-platinum' },
{ name: 'Diamond', color: 'var(--primary)', class: 'rank-diamond' },
{ name: 'Obsidian', color: '#ff3e3e', class: 'rank-obsidian' }
];
const vipLevel = computed(() => {
if (user.value?.vip_level !== undefined) return parseInt(user.value.vip_level);
if (stats.value.vip_level !== undefined) return parseInt(stats.value.vip_level);
return 0;
});
const currentVipStyle = computed(() => {
const idx = Math.min(Math.max(vipLevel.value, 0), vipLevelsConfig.length - 1);
return vipLevelsConfig[idx];
});
const vipPoints = computed(() => parseFloat(stats.value.vip_points || 0));
const vipProgress = computed(() => {
const points = vipPoints.value;
const pointsPerLevel = 1000;
const currentLevelPoints = points % pointsPerLevel;
return (currentLevelPoints / pointsPerLevel) * 100;
});
// --- Sidebar Logic ---
const isSidebarOpen = ref(false);
const isSidebarCollapsed = ref(false);
const toggleMenu = () => { isSidebarOpen.value = !isSidebarOpen.value; };
const toggleSidebar = () => { isSidebarCollapsed.value = !isSidebarCollapsed.value; };
const forceCollapseSidebar = () => { isSidebarCollapsed.value = true; };
const forceExpandSidebar = () => { isSidebarCollapsed.value = false; };
// --- Auth Modal Logic ---
const showLoginModal = ref(false);
const showRegisterModal = ref(false);
const openLogin = () => {
showLoginModal.value = true;
showRegisterModal.value = false;
};
const openRegister = () => {
showRegisterModal.value = true;
showLoginModal.value = false;
};
const closeAuthModals = () => {
showLoginModal.value = false;
showRegisterModal.value = false;
};
const handleAuthSwitch = (type: 'login' | 'register') => {
if (type === 'login') openLogin();
else openRegister();
};
// Global event listeners for auth modals
onMounted(() => {
window.addEventListener('require-login', openLogin);
window.addEventListener('require-register', openRegister);
});
onUnmounted(() => {
window.removeEventListener('require-login', openLogin);
window.removeEventListener('require-register', openRegister);
});
// --- Wallet Logic ---
const isBalanceAnimatingOut = ref(false);
const isBalanceAnimatingIn = ref(false);
const selectedCurrencyCode = ref('BTX');
const CURRENCY_META: 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: 'Polygon', icon: 'hexagon', color: '#8247e5' },
'BCH': { name: 'Bitcoin Cash', icon: 'bitcoin', color: '#8dc351' },
'SHIB': { name: 'Shiba Inu', icon: 'paw-print', color: '#e1b303' },
'DOT': { name: 'Polkadot', icon: 'waypoints', color: '#e6007a' },
'AVAX': { name: 'Avalanche', icon: 'triangle', color: '#e84142' },
'BTX': { name: 'BetiX Coin', icon: 'gem', color: 'var(--primary)' },
};
// Currencies loaded from NowPayments (enabled list) or fallback
const enabledCurrencies = ref<string[]>([]);
async function loadWalletCurrencies() {
try {
const resp = await fetch('/wallet/deposits/currencies', {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
if (!resp.ok) throw new Error('failed');
const json = await resp.json();
if (Array.isArray(json.enabled) && json.enabled.length) {
enabledCurrencies.value = json.enabled;
}
} catch {
// keep empty — wallets computed falls back to BTC/ETH/SOL
}
}
const wallets = computed(() => {
const cryptos = enabledCurrencies.value.length
? enabledCurrencies.value
: ['BTC', 'ETH', 'SOL'];
const list = cryptos.map((ticker: string) => {
const meta = CURRENCY_META[ticker] || { name: ticker, icon: 'badge-cent', color: '#a1a1aa' };
return { currency: ticker, name: meta.name, amount: 0, icon: meta.icon, color: meta.color };
});
// BTX always at the end
const btxMeta = CURRENCY_META['BTX'];
list.push({ currency: 'BTX', name: btxMeta.name, amount: parseFloat(user.value?.balance || '0'), icon: btxMeta.icon, color: btxMeta.color });
// Update amounts from user.wallets if available
if (user.value?.wallets) {
user.value.wallets.forEach((w: any) => {
const item = list.find((l: any) => l.currency === w.currency);
if (item) item.amount = parseFloat(w.balance);
});
}
return list;
});
const currentWallet = computed(() => {
return wallets.value.find(w => w.currency === selectedCurrencyCode.value)
|| wallets.value.find(w => w.currency === 'BTX')
|| wallets.value[0];
});
onMounted(() => {
const btx = wallets.value.find(w => w.currency === 'BTX');
if (!btx || btx.amount === 0) {
const highest = wallets.value.reduce((prev, current) => (prev.amount > current.amount) ? prev : current);
if (highest.amount > 0) {
selectedCurrencyCode.value = highest.currency;
}
}
});
const selectCurrency = (code: string) => {
if (selectedCurrencyCode.value === code) return;
isBalanceAnimatingOut.value = true;
setTimeout(() => {
selectedCurrencyCode.value = code;
isBalanceAnimatingOut.value = false;
isBalanceAnimatingIn.value = true;
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
setTimeout(() => { isBalanceAnimatingIn.value = false; }, 300);
}, 200);
};
// --- Notifications & Dropdowns ---
const { toasts, history, closeToast, markAllRead, clearAll } = useNotifications();
const showNotifDropdown = ref(false);
const isProfileOpen = ref(false);
const isLangOpen = ref(false);
// Primary color control
const { primaryColor, updatePrimaryColor } = usePrimaryColor();
const onPrimaryColorInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value;
updatePrimaryColor(value);
};
// Ensure primary color is set
onMounted(() => {
if (!primaryColor.value || primaryColor.value === '#ff007a') {
updatePrimaryColor('#DF006A');
}
});
// --- Dark / Light Mode ---
const isLightMode = ref(false);
function applyTheme(light: boolean) {
document.documentElement.setAttribute('data-theme', light ? 'light' : 'dark');
}
function toggleTheme() {
isLightMode.value = !isLightMode.value;
try { localStorage.setItem('casino-theme', isLightMode.value ? 'light' : 'dark'); } catch {}
applyTheme(isLightMode.value);
}
onMounted(() => {
try {
const saved = localStorage.getItem('casino-theme');
if (saved === 'light') {
isLightMode.value = true;
applyTheme(true);
}
} catch {}
});
// Server notifications (database) shared via Inertia
const serverNotifications = computed(() => (page.props as any).serverNotifications || []);
const serverUnreadCount = computed(() => (page.props as any).serverUnreadCount || 0);
function acceptFriend(id: number) {
router.post(`/friends/${id}/accept`, {}, {
preserveScroll: true,
onSuccess: () => router.reload({ only: ['auth'] })
});
}
function declineFriend(id: number) {
router.post(`/friends/${id}/decline`, {}, {
preserveScroll: true,
onSuccess: () => router.reload({ only: ['auth'] })
});
}
// Refs for click-outside detection
const notifWrapper = ref<HTMLElement | null>(null);
const profileWrapper = ref<HTMLElement | null>(null);
const langWrapper = ref<HTMLElement | null>(null);
const walletWrapperRef = ref<HTMLElement | null>(null);
const walletMenuOpen = ref(false);
const toggleNotifDropdown = () => {
showNotifDropdown.value = !showNotifDropdown.value;
if (showNotifDropdown.value) markAllRead();
};
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (notifWrapper.value && !notifWrapper.value.contains(target)) showNotifDropdown.value = false;
if (profileWrapper.value && !profileWrapper.value.contains(target)) isProfileOpen.value = false;
if (langWrapper.value && !langWrapper.value.contains(target)) isLangOpen.value = false;
if (walletWrapperRef.value && !walletWrapperRef.value.contains(target)) walletMenuOpen.value = false;
};
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleGlobalKeydown);
document.removeEventListener('collapse-sidebar', forceCollapseSidebar);
document.removeEventListener('expand-sidebar', forceExpandSidebar);
});
// Global Chat & Vault
const isChatOpen = ref(false);
const chatUnread = ref(0);
const isVaultOpen = ref(false);
// --- Search Modal ---
const isSearchOpen = ref(false);
const handleGlobalKeydown = (e: KeyboardEvent) => {
// Ctrl+K or Cmd+K to open search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
isSearchOpen.value = true;
}
};
// --- Locale switcher ---
const availableLocales = computed(() => (page.props as any).availableLocales || []);
const currentLocale = computed(() => (page.props as any).locale || 'en');
async function changeLocale(code: string) {
try {
await csrfFetch('/locale', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ locale: code })
});
await setI18nLocale(code);
router.reload({ only: ['locale'] });
isLangOpen.value = false;
} catch {}
}
// --- Nav Categories ---
const navCategories = ref({
gaming: true,
user: true,
support: true
});
const toggleCategory = (cat: keyof typeof navCategories.value) => {
navCategories.value[cat] = !navCategories.value[cat];
};
const logout = () => {
const postUrl = typeof (window as any).route === 'function' ? (window as any).route('logout') : '/logout';
router.post(postUrl);
};
// --- REAL MODE BALANCE SYNC ---
const balanceInterval = ref<any>(null);
const fetchBalance = async () => {
if (!user.value) return;
try {
const response = await fetch('/api/wallet/balance', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.balance !== undefined && user.value.balance !== data.balance) {
user.value.balance = data.balance;
}
if (data.wallets && user.value.wallets) {
user.value.wallets = data.wallets;
}
}
} catch (e) {
console.error('Balance sync failed:', e);
}
};
// Adaptive polling: 5 s on game pages, 30 s elsewhere
function getPollingInterval(): number {
return window.location.pathname.startsWith('/games/play/') ? 5000 : 30000;
}
function restartBalanceInterval() {
if (balanceInterval.value) clearInterval(balanceInterval.value);
balanceInterval.value = setInterval(fetchBalance, getPollingInterval());
}
// Immediate refresh triggered by game iframe / round webhook signal
function onBalanceRefresh() {
fetchBalance();
restartBalanceInterval();
}
onMounted(() => {
restartBalanceInterval();
fetchBalance();
loadWalletCurrencies();
document.addEventListener('balance:refresh', onBalanceRefresh);
});
onUnmounted(() => {
if (balanceInterval.value) clearInterval(balanceInterval.value);
document.removeEventListener('balance:refresh', onBalanceRefresh);
});
watch(() => currentWallet.value.icon, () => {
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
});
// Mobile: close sidebar with ESC and prevent background scroll when open
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && isSidebarOpen.value) {
isSidebarOpen.value = false;
}
}
watch(isSidebarOpen, (open) => {
try {
const isMobile = window.matchMedia('(max-width: 1000px)').matches;
if (isMobile) {
document.body.style.overflow = open ? 'hidden' : '';
}
} catch {}
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
try { document.body.style.overflow = ''; } catch {}
});
// Prevent clicks inside the sidebar from bubbling to document
function onSidebarClick(e: MouseEvent) {
e.stopPropagation();
}
// Auto-close sidebar on route change
watch(() => page.url, () => {
if (isSidebarOpen.value) isSidebarOpen.value = false;
});
</script>
<template>
<div class="app-container">
<AppLoading />
<Notification :toasts="toasts" @close="closeToast" />
<div class="app">
<!-- SIDEBAR -->
<aside :class="['sidebar', { open: isSidebarOpen, collapsed: isSidebarCollapsed }]" id="sidebar" @click="onSidebarClick">
<div class="sidebar-header">
<div class="brand">Beti<span>X</span></div>
<button class="collapse-btn" @click="toggleSidebar">
<i data-lucide="panel-left"></i>
</button>
<button class="close-mobile" @click="isSidebarOpen = false">
<i data-lucide="x"></i>
</button>
</div>
<!-- Profile Card -->
<div class="profile-card" v-if="user" :class="[currentVipStyle.class, user.role === 'admin' && 'role-admin']">
<div class="pc-bg">
<div class="pc-inner">
<Link :href="`/social/${user.username}`" class="pc-avatar-wrap">
<div class="pc-avatar">
<img v-if="user.avatar || user.avatar_url"
:src="user.avatar || user.avatar_url"
alt="Avatar"
@error="($event.target as HTMLImageElement).style.display = 'none'">
<span v-else>{{ (user.username || user.name || '?').charAt(0).toUpperCase() }}</span>
</div>
<span class="pc-status-dot" :style="{ background: currentVipStyle.color }"></span>
<span v-if="user.role === 'admin'" class="pc-crown"><i data-lucide="crown"></i></span>
</Link>
<div class="pc-info">
<Link :href="`/social/${user.username}`" class="pc-name">{{ user.username || user.name }}</Link>
<div class="pc-meta">
<span v-if="user.role === 'admin'" class="pc-badge-admin"><i data-lucide="crown"></i> Admin</span>
<span class="pc-badge-vip" :style="{ color: currentVipStyle.color }">
<span class="pc-dot" :style="{ background: currentVipStyle.color }"></span>
{{ currentVipStyle.name }}
</span>
</div>
<div class="pc-progress">
<div class="pc-track"><div class="pc-fill" :style="{ width: `${vipProgress}%` }"></div></div>
<div class="pc-xp">LVL {{ vipLevel }} · {{ Math.floor(vipPoints % 1000) }}/1000 XP</div>
</div>
</div>
</div>
</div>
</div>
<!-- Guest Card -->
<div class="profile-card guest" v-else>
<div class="pc-bg">
<div class="pc-inner">
<div class="pc-avatar-wrap" style="pointer-events:none">
<div class="pc-avatar pc-avatar--guest"><i data-lucide="user"></i></div>
</div>
<div class="pc-info">
<span class="pc-name" style="color:#555">{{ $t('common.guest') }}</span>
<div class="pc-guest-btns">
<button class="pca-guest-login" @click="openLogin">{{ $t('auth.login') }}</button>
<button class="pca-guest-reg" @click="openRegister">{{ $t('auth.register') }}</button>
</div>
</div>
</div>
</div>
</div>
<Link href="/wallet" class="sidebar-deposit-btn">
<div class="btn-content">
<i data-lucide="plus-circle"></i>
<span class="btn-text">{{ $t('topbar.deposit') }}</span>
</div>
<div class="btn-shine"></div>
</Link>
<nav class="nav-menu">
<!-- Gaming -->
<div class="nav-group">
<div class="nav-head" @click="toggleCategory('gaming')">
<span class="nav-cat-title">{{ $t('sections.gaming') }}</span>
<i data-lucide="chevron-down" :class="{ rotated: !navCategories.gaming }"></i>
</div>
<div class="nav-items" :class="{ collapsed: !navCategories.gaming }">
<Link href="/dashboard" :class="{ active: $page.url === '/dashboard' }">
<div class="nav-icon"><i data-lucide="layout"></i></div>
<span class="nav-text">{{ $t('nav.lobby') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/dashboard" :class="{ active: $page.url === '/live' }">
<div class="nav-icon"><i data-lucide="clapperboard"></i></div>
<span class="nav-text">{{ $t('nav.liveCasino') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/dashboard" :class="{ active: $page.url === '/slots' }">
<div class="nav-icon"><i data-lucide="disc"></i></div>
<span class="nav-text">{{ $t('nav.slots') }}</span>
<div class="active-glow"></div>
</Link>
</div>
</div>
<!-- User -->
<div class="nav-group">
<div class="nav-head" @click="toggleCategory('user')">
<span class="nav-cat-title">{{ $t('sections.user') }}</span>
<i data-lucide="chevron-down" :class="{ rotated: !navCategories.user }"></i>
</div>
<div class="nav-items" :class="{ collapsed: !navCategories.user }">
<Link href="/bonuses" :class="{ active: $page.url === '/bonuses' }">
<div class="nav-icon"><i data-lucide="gift"></i></div>
<span class="nav-text">{{ $t('nav.bonuses') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/wallet" :class="{ active: $page.url === '/wallet' }">
<div class="nav-icon"><i data-lucide="wallet"></i></div>
<span class="nav-text">{{ $t('nav.wallet') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/vip-levels" :class="{ active: $page.url === '/vip-levels' }">
<div class="nav-icon"><i data-lucide="crown"></i></div>
<span class="nav-text">{{ $t('nav.vip') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/guilds" :class="{ active: $page.url.startsWith('/guilds') }">
<div class="nav-icon"><i data-lucide="shield"></i></div>
<span class="nav-text">{{ $t('nav.guilds') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/social" :class="{ active: $page.url === '/social' }">
<div class="nav-icon"><i data-lucide="users"></i></div>
<span class="nav-text">Social</span>
<div class="active-glow"></div>
</Link>
<Link href="/trophy" :class="{ active: $page.url === '/trophy' }">
<div class="nav-icon"><i data-lucide="trophy"></i></div>
<span class="nav-text">Trophy Room</span>
<div class="active-glow"></div>
</Link>
</div>
</div>
<!-- Support -->
<div class="nav-group">
<div class="nav-head" @click="toggleCategory('support')">
<span class="nav-cat-title">{{ $t('sections.supportInfo') }}</span>
<i data-lucide="chevron-down" :class="{ rotated: !navCategories.support }"></i>
</div>
<div class="nav-items" :class="{ collapsed: !navCategories.support }">
<Link href="/faq" :class="{ active: $page.url === '/faq' }">
<div class="nav-icon"><i data-lucide="help-circle"></i></div>
<span class="nav-text">{{ $t('nav.faq') }}</span>
<div class="active-glow"></div>
</Link>
<Link href="/self-exclusion" :class="{ active: $page.url === '/self-exclusion' }">
<div class="nav-icon"><i data-lucide="alert-octagon"></i></div>
<span class="nav-text">{{ $t('nav.responsible') }}</span>
<div class="active-glow"></div>
</Link>
</div>
</div>
</nav>
</aside>
<div v-if="isSidebarOpen" class="sidebar-backdrop" @click="isSidebarOpen = false"></div>
<main class="main" :class="{ expanded: isSidebarCollapsed }">
<header class="topbar">
<div class="tb-left">
<div class="mobile-toggle" @click="toggleMenu"><i data-lucide="menu"></i></div>
<button class="header-search-bar" @click="isSearchOpen = true" aria-label="Suche öffnen">
<i data-lucide="search" class="hsr-icon"></i>
<span class="hsr-text">Spiele, Provider, @User suchen...</span>
<kbd class="hsr-kbd">K</kbd>
</button>
</div>
<!-- Centered Wallet Display -->
<div class="wallet-wrapper" ref="walletWrapperRef" :class="{ open: walletMenuOpen }" @mouseenter="walletMenuOpen = true" @mouseleave="walletMenuOpen = false">
<div class="wallet-pill" id="wallet-btn" @click.stop="walletMenuOpen = !walletMenuOpen" role="button" tabindex="0" aria-haspopup="true" :aria-expanded="walletMenuOpen">
<div class="wp-icon-box" :style="{ borderColor: currentWallet.color, boxShadow: `0 0 10px ${currentWallet.color}40` }">
<i :data-lucide="currentWallet.icon" :style="{ color: currentWallet.color }"></i>
</div>
<div class="wp-info">
<span id="balance-display" :class="{ 'anim-out': isBalanceAnimatingOut, 'anim-in': isBalanceAnimatingIn }">
{{ currentWallet.amount.toFixed(4) }}
</span>
</div>
<div class="wp-actions">
<button class="wp-btn vault" @click="isVaultOpen = true" title="Vault">
<i data-lucide="lock"></i>
</button>
<button class="wp-btn deposit" @click="$inertia.visit('/wallet')" title="Deposit">
<i data-lucide="plus"></i>
</button>
</div>
</div>
<!-- Currency Dropdown -->
<div class="wallet-dropdown">
<div
v-for="coin in wallets"
:key="coin.currency"
class="wd-item"
@click="selectCurrency(coin.currency)"
:class="{ active: selectedCurrencyCode === coin.currency }"
>
<div class="wd-left">
<i :data-lucide="coin.icon" :style="{ color: coin.color }"></i>
<span>{{ coin.name }}</span>
</div>
<div class="wd-right">{{ coin.amount.toFixed(4) }}</div>
</div>
</div>
</div>
<div class="h-actions">
<SearchModal v-if="isSearchOpen" @close="isSearchOpen = false" />
<!-- Language Switcher -->
<div class="lang-wrapper" ref="langWrapper">
<button class="lang-btn" :class="{ active: isLangOpen }" @click="isLangOpen = !isLangOpen">
<img :src="(availableLocales.find(l=>l.code===currentLocale)?.flag) || 'https://flagcdn.com/w40/gb.png'" class="flag">
<i data-lucide="chevron-down" class="lang-arrow"></i>
</button>
<transition name="pop-down">
<div v-if="isLangOpen" class="lang-menu">
<button v-for="loc in availableLocales" :key="loc.code" class="lang-opt" @click.stop="changeLocale(loc.code)">
<img :src="loc.flag" class="flag"> {{ loc.label }}
</button>
</div>
</transition>
</div>
<button class="h-icon-btn chat-btn" :class="{ 'has-unread': chatUnread > 0 }" title="Global Chat" @click="isChatOpen = true">
<i data-lucide="message-circle"></i>
<span v-if="chatUnread > 0" class="chat-badge">{{ chatUnread > 99 ? '99+' : chatUnread }}</span>
</button>
<!-- Notification Bell -->
<div class="notif-wrapper" ref="notifWrapper">
<button class="h-icon-btn" @click="toggleNotifDropdown" :class="{ active: showNotifDropdown }">
<div v-if="serverUnreadCount > 0" class="badge-dot"></div>
<i data-lucide="bell" :class="{ 'bell-shake': serverUnreadCount > 0 }"></i>
</button>
<transition name="pop-down">
<div v-if="showNotifDropdown" class="notif-dropdown">
<div class="nd-head">
<span>Notifications</span>
<!-- keep client clear for toasts history; server notifications are persisted -->
<button class="nd-clear" @click="clearAll" v-if="history.length">Clear</button>
</div>
<div class="nd-list">
<div v-if="!serverNotifications.length" class="nd-empty">No new notifications</div>
<div v-for="n in serverNotifications" :key="n.id" class="nd-item" :class="{ unread: !n.read }">
<div class="nd-icon-box"><i :data-lucide="n.icon || 'bell'"></i></div>
<div class="nd-content" style="flex:1;">
<div class="nd-title">{{ n.title }}</div>
<div class="nd-desc">{{ n.desc }}</div>
<div class="nd-time">{{ new Date(n.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) }}</div>
</div>
<div v-if="n.kind === 'friend_request' && n.friend_request_id" class="nd-actions" style="display:flex; gap:6px; align-items:center;">
<button class="h-icon-btn" title="Accept" @click.stop="acceptFriend(n.friend_request_id)"><i data-lucide="check"></i></button>
<button class="h-icon-btn" title="Decline" @click.stop="declineFriend(n.friend_request_id)"><i data-lucide="x"></i></button>
</div>
</div>
</div>
</div>
</transition>
</div>
<!-- Profile Dropdown -->
<div class="profile-wrapper" ref="profileWrapper" v-if="user">
<button class="user-avatar-btn"
@click="isProfileOpen = !isProfileOpen"
:class="['role-' + (user.role || 'user'), { active: isProfileOpen }]"
:title="user.username || user.name">
<img v-if="user.avatar || user.avatar_url" :src="user.avatar || user.avatar_url" alt="User">
<span v-else>{{ (user.username || user.name).charAt(0).toUpperCase() }}</span>
<span v-if="user.role === 'admin'" class="uab-role-dot uab-role-dot--admin">
<i data-lucide="crown"></i>
</span>
<span v-else-if="user.role === 'mod'" class="uab-role-dot uab-role-dot--mod">
<i data-lucide="shield"></i>
</span>
</button>
<transition name="pop-down">
<div v-if="isProfileOpen" class="profile-dropdown">
<div class="pd-header">
<div class="pd-name">{{ user.username || user.name }}</div>
<div class="pd-email">{{ user.email }}</div>
</div>
<div class="pd-links">
<Link href="/settings" class="pd-item">
<i data-lucide="globe"></i> {{ $t('nav.public_profile') }}
</Link>
<Link href="/settings/profile" class="pd-item">
<i data-lucide="user"></i> {{ $t('nav.account_settings') }}
</Link>
<Link href="/wallet" class="pd-item">
<i data-lucide="wallet"></i> {{ $t('nav.wallet') }}
</Link>
<Link href="/self-exclusion" class="pd-item">
<i data-lucide="shield-alert"></i> {{ $t('nav.responsible') }}
</Link>
</div>
<div class="pd-theme">
<div class="pd-theme-row">
<i data-lucide="palette"></i>
<span>Accent color</span>
<input class="pd-color" type="color" :value="primaryColor" @input="onPrimaryColorInput" />
</div>
<div class="pd-theme-row">
<i :data-lucide="isLightMode ? 'sun' : 'moon'"></i>
<span>{{ isLightMode ? 'Light Mode' : 'Dark Mode' }}</span>
<button class="theme-toggle-btn" :class="{ light: isLightMode }" @click="toggleTheme" :title="isLightMode ? 'Switch to Dark' : 'Switch to Light'">
<span class="tt-thumb"></span>
</button>
</div>
</div>
<div class="pd-footer">
<button class="pd-logout" @click="logout">
<i data-lucide="log-out"></i> {{ $t('auth.logout') }}
</button>
</div>
</div>
</transition>
</div>
<!-- Guest Actions -->
<div class="guest-actions" v-else>
<button class="auth-btn login" @click="openLogin">
{{ $t('auth.login') }}
</button>
<button class="auth-btn register" @click="openRegister">
<span class="btn-text">{{ $t('auth.register') }}</span>
<div class="btn-glow"></div>
</button>
</div>
</div>
</header>
<div class="content-wrapper">
<transition name="page-fade" mode="out-in">
<div :key="$page.url">
<slot></slot>
</div>
</transition>
</div>
<Footer />
</main>
</div>
<GlobalChat v-model:open="isChatOpen" @update:unread="chatUnread = $event" />
<SupportChat />
<!-- Mobile Bottom Navigation -->
<div class="mobile-nav-bottom">
<Link href="/dashboard" class="m-nav-item" :class="{ active: $page.url === '/dashboard' }">
<i data-lucide="layout"></i>
<span>Lobby</span>
</Link>
<Link href="/wallet" class="m-nav-item" :class="{ active: $page.url.startsWith('/wallet') }">
<i data-lucide="wallet"></i>
<span>Wallet</span>
</Link>
<button class="m-nav-item" @click="toggleMenu">
<i data-lucide="menu"></i>
<span>Menu</span>
</button>
<Link href="/trophy" class="m-nav-item" :class="{ active: $page.url === '/trophy' }">
<i data-lucide="trophy"></i>
<span>Trophies</span>
</Link>
<button class="m-nav-item" @click="isChatOpen = !isChatOpen">
<i data-lucide="message-square"></i>
<span>Chat</span>
</button>
</div>
<VaultModal :open="isVaultOpen" :coins="wallets" @close="isVaultOpen = false" />
<AuthModals
:show-login="showLoginModal"
:show-register="showRegisterModal"
@close="closeAuthModals"
@switch="handleAuthSwitch"
/>
</div>
</template>
<style>
:root {
--bg-deep: #050505;
--bg-side: #0a0a0a;
--bg-card: #0f0f0f;
--border: #1f1f1f;
--primary: #df006a; /* Unified primary color */
--primary-foreground: #ffffff;
--magenta: var(--primary); /* Keep for backwards compatibility */
--cyan: #00f2ff;
--text: #ffffff;
--muted: #888;
--radius: 16px;
--ease: cubic-bezier(0.2, 0, 0, 1);
--vip-color: var(--primary);
}
/* Light Mode */
[data-theme="light"] {
--bg-deep: #f0f1f5;
--bg-side: #ffffff;
--bg-card: #ffffff;
--border: #e2e2e7;
--text: #111111;
--muted: #555555;
}
[data-theme="light"] body { background: var(--bg-deep); color: var(--text); }
[data-theme="light"] ::-webkit-scrollbar-thumb { background: #ccc; }
[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--primary); }
/* VIP level color variables */
.rank-newbie { --vip-color: #888888; }
.rank-bronze { --vip-color: #cd7f32; }
.rank-silver { --vip-color: #c0c0c0; }
.rank-gold { --vip-color: #ffd700; }
.rank-platinum { --vip-color: #00f2ff; }
.rank-diamond { --vip-color: var(--primary); }
.rank-obsidian { --vip-color: #ff3e3e; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary); }
* { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
body { margin: 0; background: var(--bg-deep); font-family: 'Inter', sans-serif; color: var(--text); overflow-x: hidden; }
.app-container { min-height: 100vh; display: flex; flex-direction: column; }
.app { display: flex; min-height: 100vh; width: 100%; overflow-x: hidden; }
/* --- Sidebar --- */
.sidebar {
width: 260px; background: var(--bg-side); border-right: 1px solid var(--border);
position: fixed; top: 0; bottom: 0; left: 0; z-index: 1000;
display: flex; flex-direction: column; padding: 20px 16px; gap: 20px;
transition: width 0.3s var(--ease), padding 0.3s, transform 0.3s var(--ease); overflow-y: auto; overflow-x: hidden;
}
.sidebar.collapsed { width: 80px; padding: 20px 10px; }
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
/* Animated Logo */
.brand {
font-size: 24px; font-weight: 900; letter-spacing: -1px; cursor: pointer; user-select: none;
white-space: nowrap;
}
.brand span {
background: linear-gradient(to right, var(--primary) 20%, #fff 50%, var(--primary) 80%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine 4s linear infinite;
display: inline-block;
}
@keyframes shine { to { background-position: 200% center; } }
.sidebar.collapsed .brand { display: none; }
.collapse-btn {
background: transparent; border: none; color: #666; cursor: pointer; transition: 0.2s;
display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px;
}
.collapse-btn:hover { color: #fff; background: #1a1a1a; }
.sidebar.collapsed .collapse-btn { width: 100%; margin-bottom: 10px; }
.sidebar.collapsed .collapse-btn i { transform: rotate(180deg); }
/* ─── Profile Card ─────────────────────────────────── */
.profile-card {
border-radius: 13px;
position: relative;
overflow: hidden;
padding: 1.5px;
background: #111;
}
/* Spinning gradient border */
.profile-card::before {
content: '';
position: absolute;
width: 300%; height: 300%;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(223,0,106,0.35) 50deg,
var(--primary) 110deg,
rgba(223,0,106,0.35) 170deg,
transparent 230deg,
transparent 360deg
);
animation: pc-border-spin 4s linear infinite;
}
.role-admin.profile-card::before {
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(255,180,0,0.4) 50deg,
#ffcc00 110deg,
rgba(255,180,0,0.4) 170deg,
transparent 230deg,
transparent 360deg
);
animation-duration: 2.5s;
}
.profile-card.guest::before { display: none; }
@keyframes pc-border-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
/* Inner solid background sits above the spinning border */
.pc-bg {
position: relative;
z-index: 1;
border-radius: 11px;
background: linear-gradient(145deg, #141414 0%, #0d0d0d 100%);
padding: 12px 12px 10px;
}
.profile-card.guest .pc-bg {
background: #0f0f0f;
}
/* ── Row: avatar + info ── */
.pc-inner {
display: flex;
align-items: center;
gap: 10px;
}
/* ── Avatar ── */
.pc-avatar-wrap {
position: relative;
width: 46px; height: 46px;
flex-shrink: 0;
display: block; text-decoration: none;
}
.pc-avatar {
width: 100%; height: 100%; border-radius: 50%;
background: #1c1c1c; border: 2px solid #2a2a2a;
display: flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 900; color: #fff;
overflow: hidden; position: relative; z-index: 2;
}
.pc-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }
.pc-avatar--guest { color: #333; font-size: 22px; }
/* VIP status dot */
.pc-status-dot {
position: absolute; bottom: 1px; right: 1px;
width: 11px; height: 11px; border-radius: 50%;
border: 2px solid #0d0d0d;
z-index: 3;
}
/* Admin crown badge on avatar */
.pc-crown {
position: absolute; bottom: 0; right: 0; z-index: 4;
width: 18px; height: 18px; border-radius: 50%;
background: #ffcc00; border: 2px solid #0d0d0d;
display: flex; align-items: center; justify-content: center;
}
.pc-crown i { width: 9px; height: 9px; color: #111; }
/* ── Info column ── */
.pc-info {
flex: 1; min-width: 0;
display: flex; flex-direction: column; gap: 3px;
}
.pc-name {
font-size: 13px; font-weight: 800; color: #e0e0e0;
text-decoration: none;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
display: block; line-height: 1.2;
}
.pc-name:hover { color: var(--primary); }
/* ── Badges row ── */
.pc-meta { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.pc-badge-admin {
display: inline-flex; align-items: center; gap: 3px;
background: rgba(255,200,0,0.1); color: #ffcc00;
border: 1px solid rgba(255,200,0,0.2);
border-radius: 99px; padding: 1px 6px;
font-size: 9px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.5px;
}
.pc-badge-admin i { width: 8px; height: 8px; }
.pc-badge-vip {
display: inline-flex; align-items: center; gap: 4px;
font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px;
}
.pc-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
/* ── XP Progress ── */
.pc-progress { display: flex; flex-direction: column; gap: 3px; }
.pc-track { height: 3px; background: rgba(255,255,255,0.06); border-radius: 99px; overflow: hidden; }
.pc-fill {
height: 100%; border-radius: 99px;
background: var(--vip-color);
box-shadow: 0 0 6px var(--vip-color);
transition: width 1.4s cubic-bezier(0.22,1,0.36,1);
min-width: 3px;
}
.pc-xp { font-size: 9px; color: #484848; font-weight: 700; letter-spacing: 0.2px; }
/* ── Action buttons ── */
.pc-actions {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px;
margin-top: 8px; padding-top: 8px;
border-top: 1px solid rgba(255,255,255,0.04);
}
.pca {
height: 30px; border-radius: 7px;
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
color: #3e3e3e; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.15s; text-decoration: none;
}
.pca i { width: 14px; height: 14px; }
.pca:hover { background: rgba(255,255,255,0.07); color: #bbb; border-color: rgba(255,255,255,0.1); }
.pca--primary:hover { background: rgba(223,0,106,0.1); color: var(--primary); border-color: rgba(223,0,106,0.15); }
/* ── Collapsed sidebar: avatar only ── */
.sidebar.collapsed .profile-card { background: transparent; padding: 0; overflow: visible; }
.sidebar.collapsed .profile-card::before { display: none; }
.sidebar.collapsed .pc-bg { background: transparent; padding: 4px 0; }
.sidebar.collapsed .pc-avatar-wrap { width: 42px; height: 42px; }
.sidebar.collapsed .pc-info,
.sidebar.collapsed .pc-actions { display: none; }
.sidebar.collapsed .pc-inner { justify-content: center; }
/* ── Guest buttons ── */
.pc-guest-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-top: 2px; }
.pca-guest-login {
height: 30px; border-radius: 7px; background: transparent;
border: 1px solid rgba(255,255,255,0.1); color: #555;
font-size: 11px; font-weight: 700; cursor: pointer; transition: 0.2s;
}
.pca-guest-login:hover { background: rgba(255,255,255,0.06); color: #bbb; }
.pca-guest-reg {
height: 30px; border-radius: 7px;
background: linear-gradient(135deg, var(--primary), #a3004d);
border: none; color: #fff; font-size: 11px; font-weight: 800;
cursor: pointer; box-shadow: 0 3px 12px rgba(223,0,106,0.25); transition: 0.2s;
}
.pca-guest-reg:hover { transform: translateY(-1px); box-shadow: 0 5px 18px rgba(223,0,106,0.4); }
/* Deposit Button - Fixed */
.sidebar-deposit-btn {
display: flex; align-items: center; justify-content: center; gap: 10px;
height: 48px; width: 100%;
background: linear-gradient(90deg, var(--primary), #a3004d); border-radius: 12px;
color: #fff; text-decoration: none; font-weight: 800; font-size: 13px;
position: relative; overflow: hidden; transition: 0.3s;
box-shadow: 0 4px 20px rgba(223,0,106,0.25);
}
.sidebar-deposit-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(223,0,106,0.4); }
.btn-content { display: flex; align-items: center; gap: 8px; z-index: 2; }
.btn-shine {
position: absolute; top: 0; left: -100%; width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
transform: skewX(-20deg); animation: btn-shine 3s infinite; z-index: 1;
}
@keyframes btn-shine { 0% { left: -100%; } 20% { left: 200%; } 100% { left: 200%; } }
.sidebar.collapsed .sidebar-deposit-btn { padding: 0; width: 48px; height: 48px; margin: 0 auto; }
.sidebar.collapsed .btn-text { display: none; }
.sidebar.collapsed .btn-content { gap: 0; }
/* Nav Menu - Enhanced Hover/Active */
.nav-menu { display: flex; flex-direction: column; gap: 6px; flex: 1; }
.nav-group { margin-bottom: 6px; }
.nav-head {
padding: 10px 12px; font-size: 10px; font-weight: 800; color: #555; text-transform: uppercase; letter-spacing: 1px;
cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: 0.2s;
border-radius: 8px;
}
.nav-head:hover { color: #fff; background: rgba(255,255,255,0.03); }
.nav-head i { width: 12px; height: 12px; transition: transform 0.3s; }
.nav-head i.rotated { transform: rotate(-90deg); }
.sidebar.collapsed .nav-head { justify-content: center; padding: 10px 0; }
.sidebar.collapsed .nav-cat-title, .sidebar.collapsed .nav-head i { display: none; }
.sidebar.collapsed .nav-head::after { content: '•'; color: #444; font-size: 16px; }
.nav-items { display: flex; flex-direction: column; gap: 4px; overflow: hidden; transition: max-height 0.3s ease; }
.nav-items.collapsed { display: none; }
.sidebar.collapsed .nav-items { display: flex !important; }
.nav-items a {
display: flex; align-items: center; gap: 12px; padding: 10px 12px;
color: #888; text-decoration: none; font-size: 13px; font-weight: 600;
border-radius: 10px; transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); position: relative; overflow: hidden;
}
/* Hover Effect */
.nav-items a:hover { color: #fff; background: #161616; padding-left: 16px; }
.nav-items a:hover .nav-icon { color: var(--primary); transform: scale(1.1); }
/* Active State */
.nav-items a.active { background: linear-gradient(90deg, rgba(223,0,106,0.1), transparent); color: #fff; }
.nav-items a.active .nav-icon { color: var(--primary); filter: drop-shadow(0 0 5px rgba(223,0,106,0.5)); }
.nav-items a.active::before {
content: ''; position: absolute; left: 0; top: 15%; bottom: 15%; width: 3px;
background: var(--primary); border-radius: 0 4px 4px 0; box-shadow: 2px 0 10px var(--primary);
}
.nav-icon { width: 20px; display: flex; justify-content: center; flex-shrink: 0; transition: 0.2s; }
.nav-icon i { width: 18px; height: 18px; }
.sidebar.collapsed .nav-items a { justify-content: center; padding: 12px 0; }
.sidebar.collapsed .nav-text { display: none; }
.sidebar.collapsed .nav-items a:hover { transform: none; background: #1a1a1a; padding: 12px 0; }
.sidebar-footer { margin-top: auto; font-size: 9px; font-weight: 800; color: #333; display: flex; align-items: center; justify-content: center; gap: 6px; letter-spacing: 1px; padding-top: 20px; }
.sidebar-footer i { width: 12px; }
.sidebar.collapsed .footer-text { display: none; }
/* Mobile close button inside sidebar header */
.close-mobile { display: none; }
@media (max-width: 1000px) {
.close-mobile {
display: inline-flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border: none; background: transparent; color: #777; border-radius: 8px;
}
.close-mobile:hover { background: #1a1a1a; color: #fff; }
}
/* Backdrop for mobile sidebar */
.sidebar-backdrop {
display: none;
}
@media (max-width: 1000px) {
.sidebar-backdrop {
display: block; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 900;
backdrop-filter: blur(2px);
}
}
/* --- Main Content --- */
.main { margin-left: 260px; flex: 1; display: flex; flex-direction: column; width: calc(100% - 260px); min-width: 0; transition: margin-left 0.3s var(--ease), width 0.3s var(--ease); }
.main.expanded { margin-left: 80px; width: calc(100% - 80px); }
.topbar {
height: 76px; padding: 0 32px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
background: rgba(5,5,5,0.85); backdrop-filter: blur(16px); border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 900;
}
.tb-left { display: flex; align-items: center; gap: 12px; }
/* Header search bar */
.header-search-bar {
display: flex; align-items: center; gap: 10px;
height: 40px; padding: 0 14px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px;
cursor: pointer; transition: .2s;
color: #555; font-size: 13px;
min-width: 240px; max-width: 320px;
}
.header-search-bar:hover {
border-color: rgba(223,0,106,.3);
background: rgba(223,0,106,.04);
color: #888;
box-shadow: 0 0 0 3px rgba(223,0,106,.06);
}
.hsr-icon { width: 15px; height: 15px; flex-shrink: 0; color: #444; transition: color .2s; }
.header-search-bar:hover .hsr-icon { color: var(--primary, #df006a); }
.hsr-text { flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; }
.hsr-kbd {
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1);
border-radius: 5px; padding: 1px 6px; font-size: 10px;
color: #444; font-family: monospace; flex-shrink: 0; letter-spacing: .5px;
}
/* Wallet Pill - Centered */
.wallet-wrapper { position: relative; display: flex; justify-content: center; }
.wallet-pill {
display: flex; align-items: center; background: #0a0a0a; border: 1px solid #222;
border-radius: 14px; padding: 4px; height: 48px; transition: 0.3s; gap: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
.wallet-pill:hover { border-color: #333; box-shadow: 0 8px 30px rgba(0,0,0,0.4); transform: translateY(-1px); }
.wp-icon-box { width: 38px; height: 38px; background: #141414; border-radius: 10px; display: flex; align-items: center; justify-content: center; border: 1px solid transparent; }
.wp-icon-box i { width: 20px; height: 20px; }
.wp-info { display: flex; flex-direction: column; justify-content: center; min-width: 80px; cursor: pointer; }
.wp-info span { font-weight: 800; font-size: 15px; color: #fff; letter-spacing: -0.5px; }
.anim-out { opacity: 0; transform: translateY(-5px); filter: blur(4px); transition: 0.2s; }
.anim-in { opacity: 1; transform: translateY(0); filter: blur(0); transition: 0.2s; }
.wp-actions { display: flex; gap: 4px; padding-right: 4px; }
.wp-btn {
width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
color: #666; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: 0.2s;
}
.wp-btn:hover { background: #1a1a1a; color: #fff; }
.wp-btn.vault:hover { color: var(--primary); background: rgba(223,0,106,0.1); }
.wp-btn.deposit:hover { color: var(--cyan); background: rgba(0,242,255,0.1); }
.wp-btn i { width: 16px; height: 16px; }
.wallet-dropdown {
position: absolute; top: 120%; left: 50%; transform: translateX(-50%) translateY(10px); width: min(90vw, 260px); background: #0f0f0f; border: 1px solid #222;
border-radius: 16px; padding: 8px; opacity: 0; visibility: hidden;
transition: 0.2s cubic-bezier(0.2, 0, 0, 1); box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 100;
max-height: min(60vh, 420px); overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.wallet-wrapper.open .wallet-dropdown { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
.wd-item { display: flex; justify-content: space-between; padding: 12px; border-radius: 10px; cursor: pointer; font-size: 13px; font-weight: 700; color: #888; transition: 0.2s; }
.wd-item:hover { background: #18181b; color: #fff; }
.wd-item.active { background: #18181b; color: #fff; }
.wd-left { display: flex; align-items: center; gap: 10px; }
.wd-left i { width: 16px; }
/* Header Actions */
.h-actions { display: flex; align-items: center; gap: 12px; justify-content: flex-end; }
.h-icon-btn {
width: 42px; height: 42px; border-radius: 12px; background: #0a0a0a; border: 1px solid #222;
color: #888; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; position: relative;
}
.h-icon-btn:hover, .h-icon-btn.active { background: #18181b; color: #fff; border-color: #333; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.h-icon-btn i { width: 20px; height: 20px; }
.h-icon-btn.has-unread { border-color: rgba(223,0,106,0.4); color: #df006a; animation: chat-pulse 2s ease-in-out infinite; }
@keyframes chat-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(223,0,106,0); } 50% { box-shadow: 0 0 0 4px rgba(223,0,106,0.2); } }
.chat-badge {
position: absolute; top: -5px; right: -5px;
background: #df006a; color: #fff;
font-size: 9px; font-weight: 900; line-height: 1;
padding: 2px 4px; border-radius: 20px; min-width: 16px; text-align: center;
border: 1.5px solid #0a0a0a; pointer-events: none;
animation: badge-pop .2s cubic-bezier(0.34,1.56,0.64,1);
}
@keyframes badge-pop { from { transform: scale(0); } to { transform: scale(1); } }
.badge-dot { position: absolute; top: 10px; right: 10px; width: 8px; height: 8px; background: var(--primary); border-radius: 50%; box-shadow: 0 0 8px var(--primary); border: 2px solid #0a0a0a; }
.bell-shake { animation: bell-shake 2s infinite; }
@keyframes bell-shake { 0%, 100% { transform: rotate(0); } 10%, 30%, 50%, 70%, 90% { transform: rotate(10deg); } 20%, 40%, 60%, 80% { transform: rotate(-10deg); } }
/* Language */
.lang-wrapper { position: relative; }
.lang-btn {
height: 42px; padding: 0 12px; background: #0a0a0a; border: 1px solid #222; border-radius: 12px;
display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s;
}
.lang-btn:hover, .lang-btn.active { background: #18181b; border-color: #333; }
.lang-arrow { width: 14px; color: #666; }
.flag { width: 20px; height: 14px; border-radius: 3px; object-fit: cover; }
/* Guest Actions */
.guest-actions { display: flex; align-items: center; gap: 8px; }
.auth-btn {
height: 42px; padding: 0 20px; border-radius: 12px; font-weight: 700; font-size: 13px;
cursor: pointer; transition: 0.2s; border: 1px solid transparent;
}
.auth-btn.login {
background: transparent; color: #888; border-color: #222;
}
.auth-btn.login:hover {
color: #fff; border-color: #444; background: #111;
}
.auth-btn.register {
background: var(--primary); color: #fff; position: relative; overflow: hidden;
}
.auth-btn.register:hover {
transform: translateY(-2px); box-shadow: 0 4px 15px rgba(223,0,106,0.3);
}
.btn-glow {
position: absolute; inset: 0; background: linear-gradient(45deg, transparent, rgba(255,255,255,0.2), transparent);
transform: translateX(-100%); transition: 0.5s;
}
.auth-btn.register:hover .btn-glow { transform: translateX(100%); }
.lang-menu {
position: absolute; top: 120%; right: 0; width: 160px; background: #0f0f0f; border: 1px solid #222;
border-radius: 12px; padding: 6px; box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 100;
}
.lang-opt {
width: 100%; display: flex; align-items: center; gap: 10px; padding: 10px; border: none; background: transparent;
color: #888; font-size: 13px; font-weight: 600; border-radius: 8px; cursor: pointer; transition: 0.2s;
}
.lang-opt:hover { background: #18181b; color: #fff; }
/* User Avatar & Dropdown */
.profile-wrapper { position: relative; }
.user-avatar-btn {
width: 42px; height: 42px; background: #18181b; border-radius: 12px; display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: 16px; color: #fff; border: 1px solid #2a2a2a; cursor: pointer; transition: 0.2s;
overflow: visible; position: relative; flex-shrink: 0;
}
.user-avatar-btn img { width: 100%; height: 100%; object-fit: cover; border-radius: 11px; }
.user-avatar-btn span:not(.uab-role-dot) { pointer-events: none; }
.user-avatar-btn:hover, .user-avatar-btn.active { border-color: #444; transform: scale(1.05); box-shadow: 0 0 16px rgba(255,255,255,0.1); }
/* Admin ring */
.user-avatar-btn.role-admin { border-color: rgba(255,200,0,0.5); box-shadow: 0 0 12px rgba(255,180,0,0.25); }
.user-avatar-btn.role-mod { border-color: rgba(74,144,226,0.45); }
/* Role dot on topbar avatar */
.uab-role-dot {
position: absolute; bottom: -4px; right: -4px; z-index: 3;
width: 16px; height: 16px; border-radius: 50%; border: 2px solid #0a0a0a;
display: flex; align-items: center; justify-content: center;
}
.uab-role-dot i { width: 9px; height: 9px; }
.uab-role-dot--admin { background: #ffcc00; }
.uab-role-dot--admin i { color: #000; }
.uab-role-dot--mod { background: #4a90e2; }
.uab-role-dot--mod i { color: #fff; }
.profile-dropdown { /* container */
position: absolute; top: 130%; right: 0; width: min(92vw, 240px); background: #0f0f0f; border: 1px solid #222;
border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.9); z-index: 100;
max-height: min(70vh, 420px); overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.pd-header { padding: 16px; border-bottom: 1px solid #1a1a1a; background: #141414; }
.pd-theme { padding: 12px 16px; border-top: 1px solid #151515; border-bottom: 1px solid #151515; background: #0f0f0f; display: flex; flex-direction: column; gap: 10px; }
.pd-theme-row { display: flex; align-items: center; gap: 10px; font-size: 12px; color: #aaa; justify-content: space-between; }
.pd-theme-row i { width: 16px; height: 16px; color: var(--primary); }
.pd-color { width: 28px; height: 18px; border: 1px solid #333; background: #111; border-radius: 6px; padding: 0; cursor: pointer; }
/* Theme toggle pill */
.theme-toggle-btn {
width: 42px; height: 22px; border-radius: 11px;
background: #222; border: 1px solid #333; cursor: pointer;
position: relative; transition: background 0.25s, border-color 0.25s; padding: 0;
flex-shrink: 0;
}
.theme-toggle-btn.light { background: var(--primary); border-color: var(--primary); }
.tt-thumb {
position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px; border-radius: 50%;
background: #555; transition: transform 0.25s, background 0.25s;
display: block;
}
.theme-toggle-btn.light .tt-thumb { transform: translateX(20px); background: #fff; }
.pd-name { font-size: 14px; font-weight: 800; color: #fff; margin-bottom: 2px; }
.pd-email { font-size: 11px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-links { padding: 8px; display: flex; flex-direction: column; gap: 2px; }
.pd-item {
display: flex; align-items: center; gap: 10px; padding: 10px 12px; color: #aaa; text-decoration: none;
font-size: 13px; font-weight: 600; border-radius: 8px; transition: 0.2s;
}
.pd-item:hover { background: #1a1a1a; color: #fff; }
.pd-item i { width: 16px; height: 16px; }
.pd-footer { padding: 8px; border-top: 1px solid #1a1a1a; }
.pd-logout {
width: 100%; display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: transparent;
border: none; color: #ff3e3e; font-size: 13px; font-weight: 700; border-radius: 8px; cursor: pointer; transition: 0.2s;
}
.pd-logout:hover { background: rgba(255, 62, 62, 0.1); }
.pd-logout i { width: 16px; height: 16px; }
/* Notifications */
.notif-dropdown {
position: absolute; top: 130%; right: 0; width: min(95vw, 360px); background: #0f0f0f; border: 1px solid #222;
border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.9); z-index: 100;
max-height: min(70vh, 480px); overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.nd-head { display: flex; justify-content: space-between; padding: 16px; background: #141414; border-bottom: 1px solid #222; font-size: 12px; font-weight: 800; color: #fff; text-transform: uppercase; }
.nd-clear { background: transparent; border: none; color: #666; font-size: 11px; font-weight: 700; cursor: pointer; }
.nd-clear:hover { color: var(--primary); }
.nd-list { max-height: 350px; overflow-y: auto; }
.nd-empty { padding: 40px; text-align: center; color: #444; font-size: 13px; }
.nd-item { display: flex; gap: 14px; padding: 16px; border-bottom: 1px solid #1a1a1a; transition: 0.2s; }
.nd-item:hover { background: #141414; }
.nd-item.unread { background: rgba(223,0,106,0.03); }
.nd-icon-box { width: 36px; height: 36px; background: #1a1a1a; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #888; flex-shrink: 0; }
.nd-title { font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 4px; }
.nd-desc { font-size: 12px; color: #888; line-height: 1.4; }
.nd-time { font-size: 10px; color: #444; margin-top: 6px; text-align: right; }
/* Transitions */
.pop-down-enter-active, .pop-down-leave-active { transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); }
.pop-down-enter-from, .pop-down-leave-to { opacity: 0; transform: translateY(-10px) scale(0.95); }
/* Mobile */
.mobile-toggle { display: none; width: 40px; height: 40px; align-items: center; justify-content: center; color: #fff; cursor: pointer; }
@media (max-width: 1000px) {
.sidebar { transform: translateX(-100%); box-shadow: none; width: 280px; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
.sidebar.open { transform: translateX(0); box-shadow: 15px 0 50px rgba(0,0,0,0.9); }
.main { margin-left: 0; width: 100%; }
.main.expanded { margin-left: 0; width: 100%; }
.mobile-toggle { display: flex; }
.topbar {
padding: calc(env(safe-area-inset-top, 0) + 8px) 16px 8px 16px;
grid-template-columns: 40px 1fr 40px;
gap: 8px;
height: auto;
min-height: 64px;
z-index: 950;
}
.tb-left { gap: 8px; }
.header-search-bar { display: none; }
.wallet-wrapper { width: 100%; display: flex; justify-content: center; }
.wallet-pill { padding: 2px 4px; height: 44px; width: auto; max-width: 180px; }
.wp-icon-box { width: 34px; height: 34px; }
.wp-info { min-width: 60px; padding: 0 4px; }
.wp-info span { font-size: 13px; }
.wp-actions { display: none; }
.h-actions { gap: 8px; }
.h-actions .lang-wrapper { display: none; }
.h-actions .notif-wrapper { display: none; }
.collapse-btn { display: none; }
/* Profile card compact on mobile */
.profile-card .pc-inner { padding: 12px; }
.profile-card .pc-avatar-wrap { width: 40px; height: 40px; }
.profile-card .pc-name { font-size: 14px; }
.profile-card .pc-badge { font-size: 9px; }
/* Nav Menu adjustments */
.nav-items a { padding: 12px 16px; font-size: 14px; }
/* Deposit button responsive */
.sidebar-deposit-btn { height: 50px; font-size: 14px; margin: 10px 0; }
/* Bottom Navigation for Mobile */
.mobile-nav-bottom {
display: flex; position: fixed; bottom: 0; left: 0; right: 0; height: 64px;
background: rgba(10, 10, 10, 0.95); backdrop-filter: blur(10px);
border-top: 1px solid #222; z-index: 1000; padding: 0 10px;
justify-content: space-around; align-items: center;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.m-nav-item {
display: flex; flex-direction: column; align-items: center; gap: 4px;
color: #666; text-decoration: none; font-size: 10px; font-weight: 700;
transition: 0.2s;
}
.m-nav-item.active { color: var(--primary); }
.m-nav-item i { width: 22px; height: 22px; }
.content-wrapper { padding-bottom: 80px; } /* Space for mobile nav */
/* Auth buttons on mobile */
.auth-btn { height: 40px; padding: 0 12px; font-size: 12px; }
.auth-btn.register .btn-text { display: inline; }
}
@media (max-width: 480px) {
.topbar { grid-template-columns: 40px 1fr 40px; }
.wallet-pill { max-width: 140px; }
.wp-info span { font-size: 12px; }
.user-avatar-btn { width: 38px; height: 38px; }
}
/* Page transition */
.page-fade-enter-active { transition: opacity 0.18s ease, transform 0.18s ease; }
.page-fade-leave-active { transition: opacity 0.12s ease; }
.page-fade-enter-from { opacity: 0; transform: translateY(6px); }
.page-fade-leave-to { opacity: 0; }
</style>