Files
BetiX/resources/js/pages/Dashboard.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

1190 lines
46 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Head, router, usePage } from '@inertiajs/vue3';
import { Flame, Star, Zap, Trophy, ChevronDown, HelpCircle, PlayCircle, Heart, Shield, Gamepad2 } from 'lucide-vue-next';
import { ref, onMounted, computed, Teleport } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import Button from '@/components/ui/button.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
const page = usePage();
const authUser = computed(() => (page.props as any)?.auth?.user ?? null);
// Server-side initial games (available on first paint, no loading flicker)
const props = defineProps<{ initialGames?: any[] }>();
function mapGame(g: any, idx: number) {
const rawName = g.name ?? g.title ?? `game-${idx}`;
const rawId = g.slug ?? g.id ?? null;
return {
id: rawId ?? idx,
slug: String(rawId || rawName).toLowerCase().replace(/[^a-z0-9]+/g, '-'),
name: rawName,
provider: g.provider ?? 'BetiX',
image: g.image ?? g.thumbnail_url ?? g.thumbnail ?? '',
tag: g.tag ?? '',
rtp: g.rtp ?? 96.0,
type: g.type ?? 'original',
};
}
// --- DATA ---
const providers = ref<string[]>([]);
const slots = ref<any[]>([]);
const newReleases = ref<any[]>([]);
const liveCasino = ref<any[]>([]);
// Static Hall of Fame fallback — shown when no real bets exist yet
const bestHits = [
{ id: 1, user: 'CryptoKing', game: 'Dice', multiplier: '9,900x', amount: '5.2 BTX', image: null, rank: 1 },
{ id: 2, user: 'LuckyLuke', game: 'Mines', multiplier: '4,200x', amount: '2.1 BTX', image: null, rank: 2 },
{ id: 3, user: 'Anna_99', game: 'Crash', multiplier: '1,500x', amount: '0.8 BTX', image: null, rank: 3 },
{ id: 4, user: 'Satoshi', game: 'Plinko', multiplier: '800x', amount: '0.4 BTX', image: null, rank: 4 },
{ id: 5, user: 'Whale', game: 'Dice', multiplier: '500x', amount: '0.2 BTX', image: null, rank: 5 },
];
// Real Hall of Fame data from API
const topWins = ref<any[]>([]);
const hallOfFame = computed(() => topWins.value.length ? topWins.value : bestHits);
const faqs = [
{ q: 'How do I deposit crypto?', a: 'Go to your wallet, select the cryptocurrency you wish to deposit, and copy the address. Send your funds to that address.' },
{ q: 'Is Betix fair?', a: 'Yes, all our games use Provably Fair technology or are provided by certified, regulated game providers with RNG testing.' },
{ q: 'What is the welcome bonus?', a: 'New players get a 100% match bonus up to 1 BTC plus 50 free spins on their first deposit.' },
{ q: 'How fast are withdrawals?', a: 'Withdrawals are processed instantly. Depending on the blockchain network, it usually takes a few minutes.' },
];
// Live Wins Feed
const liveWins = ref([
{ id: 1, user: 'CryptoKing', game: 'Gates of Olympus', amount: '0.05 BTC', isWin: true },
{ id: 2, user: 'LuckyLuke', game: 'Sweet Bonanza', amount: '1.2 ETH', isWin: true },
{ id: 3, user: 'Anna_99', game: 'Razor Shark', amount: '500 USDT', isWin: true },
{ id: 4, user: 'Satoshi', game: 'Mental', amount: '0.01 BTC', isWin: true },
{ id: 5, user: 'Whale_Hunter', game: 'San Quentin', amount: '2.5 ETH', isWin: true },
{ id: 6, user: 'Dolo', game: 'Wanted', amount: '1500 XRP', isWin: true },
]);
// BetiX Originals (loaded from API)
const originals = ref<any[]>([]);
const originalsLoading = ref<boolean>(!props.initialGames?.length);
const originalsError = ref<string | null>(null);
// Iframe game session
const gameSession = ref<{ url: string; token: string; game: string } | null>(null);
const gameLoading = ref(false);
// --- STATE ---
const activeFaq = ref<number | null>(null);
const hasRealWins = ref(false);
// --- METHODS ---
const toggleFaq = (index: number) => {
activeFaq.value = activeFaq.value === index ? null : index;
};
const maskName = (name: string) => {
if (name.length <= 3) return name;
return name.substring(0, 2) + '***' + name.substring(name.length - 1);
};
const slugify = (val: string) => String(val)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const play = (game: any) => {
const user = authUser.value;
if (!user) {
window.dispatchEvent(new Event('require-login'));
return;
}
try {
const slug = game?.slug ?? game?.id ?? (game?.name ? slugify(game.name) : null);
const provider = (game?.provider ?? 'betix').toLowerCase().replace(/\s+/g, '-');
if (slug) {
router.visit(`/games/play/${encodeURIComponent(provider)}/${encodeURIComponent(String(slug))}`);
return;
}
alert('Spiel-URL nicht verfügbar.');
} catch (e) {
console.error('Play navigation failed:', e);
}
}
const providersLoading = ref<boolean>(true);
const gamesLoading = ref<boolean>(true);
const allGames = ref<any[]>((props.initialGames ?? []).map(mapGame));
const searchQuery = ref('');
const selectedProvider = ref('All');
const selectedRtpRange = ref('all'); // 'all' | 'low' | 'mid' | 'high'
const rtpRanges = [
{ value: 'all', label: 'All RTP' },
{ value: 'low', label: 'RTP < 95%' },
{ value: 'mid', label: 'RTP 9596%' },
{ value: 'high', label: 'RTP 97%+' },
];
function matchesRtp(game: any): boolean {
if (selectedRtpRange.value === 'all') return true;
const rtp = Number(game.rtp ?? 96);
if (selectedRtpRange.value === 'low') return rtp < 95;
if (selectedRtpRange.value === 'mid') return rtp >= 95 && rtp < 97;
if (selectedRtpRange.value === 'high') return rtp >= 97;
return true;
}
function matchesSearch(game: any): boolean {
return searchQuery.value === '' || game.name.toLowerCase().includes(searchQuery.value.toLowerCase());
}
function matchesProvider(game: any): boolean {
return selectedProvider.value === 'All' || game.provider === selectedProvider.value;
}
const filteredOriginals = computed(() =>
allGames.value.filter(g =>
(g.provider === 'BetiX' || g.type === 'original') &&
matchesSearch(g) && matchesProvider(g) && matchesRtp(g)
)
);
// Shows all available games in the slider — fetched automatically via loadData()
const popularSlots = computed(() =>
allGames.value.filter(g => matchesProvider(g) && matchesRtp(g)).slice(0, 12)
);
const filteredSlots = computed(() =>
allGames.value.filter(g =>
(g.type === 'slot' || !g.type) &&
matchesSearch(g) && matchesProvider(g) && matchesRtp(g)
).slice(0, 12)
);
const filteredLive = computed(() =>
allGames.value.filter(g =>
(g.type === 'live' || g.type === 'table') &&
matchesSearch(g) && matchesProvider(g) && matchesRtp(g)
).slice(0, 8)
);
// Favorites
const favorites = ref<Set<string>>(new Set());
async function loadFavorites() {
try {
const res = await fetch('/api/favorites');
if (res.ok) {
const data = await res.json();
favorites.value = new Set((data.data ?? []).map((f: any) => f.game_slug));
}
} catch {}
}
async function toggleFavorite(game: any) {
const user = authUser.value;
if (!user) { window.dispatchEvent(new Event('require-login')); return; }
const slug = game.slug ?? game.id ?? slugify(game.name ?? '');
const isFav = favorites.value.has(slug);
// Optimistic update
const next = new Set(favorites.value);
if (isFav) next.delete(slug); else next.add(slug);
favorites.value = next;
try {
if (isFav) {
await fetch(`/api/favorites/${encodeURIComponent(slug)}`, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '' } });
} else {
await fetch('/api/favorites', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '' }, body: JSON.stringify({ slug, name: game.name, image: game.image, provider: game.provider }) });
}
} catch {
// Revert on error
favorites.value = new Set(isFav ? [...favorites.value, slug] : [...favorites.value].filter(s => s !== slug));
}
}
// Recently Played
const recentlyPlayed = ref<any[]>([]);
let liveWinsErrors = 0;
let liveWinsInterval: ReturnType<typeof setInterval> | null = null;
async function loadLiveWins() {
try {
const res = await fetch('/api/wins/live', { signal: AbortSignal.timeout(4000) });
if (res.ok) {
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
liveWins.value = data.slice(0, 6);
hasRealWins.value = true;
}
liveWinsErrors = 0;
} else {
liveWinsErrors++;
}
} catch {
liveWinsErrors++;
}
// Stop polling after 3 consecutive failures — server is busy/offline
if (liveWinsErrors >= 3 && liveWinsInterval !== null) {
clearInterval(liveWinsInterval);
liveWinsInterval = null;
}
}
async function loadTopWins() {
try {
const res = await fetch('/api/wins/top');
if (res.ok) {
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
topWins.value = data;
}
}
} catch {}
}
async function loadRecentlyPlayed() {
try {
const res = await fetch('/api/recently-played');
if (res.ok) {
const data = await res.json();
recentlyPlayed.value = data.data ?? [];
}
} catch {}
}
async function loadData() {
// If we already have server-side games, don't show a loading state — just refresh in background
if (!allGames.value.length) {
providersLoading.value = true;
gamesLoading.value = true;
}
try {
// 1. BetiX Originals (always available)
let list: any[] = (props.initialGames ?? []).map(mapGame);
// 2. Try to merge in external games from backend API
try {
const res = await fetch('/api/games');
if (res.ok) {
const data = await res.json();
const external = (Array.isArray(data) ? data : (data?.games || data?.items || [])) as any[];
const externalMapped = external.map((g, i) => mapGame(g, list.length + i));
// Only add slugs not already present
const existingSlugs = new Set(list.map(g => g.slug));
list = [...list, ...externalMapped.filter(g => !existingSlugs.has(g.slug))];
}
} catch {}
// If API calls failed but we already have server-side games showing, just skip silently
if (!list.length) return;
allGames.value = list;
// Initial slices
originals.value = allGames.value.filter((g: any) => g.provider === 'BetiX' || g.type === 'original');
slots.value = allGames.value.filter((g: any) => g.type === 'slot').slice(0, 12);
liveCasino.value = allGames.value.filter((g: any) => g.type === 'live' || g.type === 'table').slice(0, 8);
newReleases.value = allGames.value.slice(0, 4);
// Providers
const pSet = new Set<string>(allGames.value.map((g: any) => g.provider).filter(Boolean));
providers.value = Array.from(pSet).sort();
} catch (e) {
console.error('Failed to load real mode lobby data:', e);
originalsError.value = t('dashboard.error_loading');
} finally {
providersLoading.value = false;
gamesLoading.value = false;
originalsLoading.value = false;
}
}
// Alias so the error-state reload button works
const loadOriginals = loadData;
let nextId = liveWins.value.length + 1;
onMounted(() => {
loadData();
loadFavorites();
loadRecentlyPlayed();
loadLiveWins();
loadTopWins();
// Poll live wins every 15s — stops automatically after 3 consecutive failures
liveWinsInterval = setInterval(loadLiveWins, 15000);
// Simulated live feed fallback — only runs when no real wins exist yet
setInterval(() => {
if (hasRealWins.value) return;
const games = ['Dice', 'Mines', 'Crash', 'Plinko'];
const users = ['Andri_X', 'CryptoKing', 'Neon_Ripper', 'Satoshi', 'Whale_Hunter', 'Dolo'];
const amounts = ['0.01 BTX', '0.5 BTX', '2.0 BTX', '0.15 BTX', '0.8 BTX'];
const newWin = {
id: nextId++,
user: users[Math.floor(Math.random() * users.length)],
game: games[Math.floor(Math.random() * games.length)],
amount: amounts[Math.floor(Math.random() * amounts.length)],
isWin: true
};
liveWins.value.unshift(newWin);
if (liveWins.value.length > 6) liveWins.value.pop();
}, 2500);
});
</script>
<template>
<UserLayout>
<Head title="Welcome" />
<div class="dashboard-content">
<!-- Hero Section -->
<div class="hero-banner">
<div class="hero-content">
<div class="badge">WELCOME OFFER</div>
<h1 class="hero-title">100% BONUS <br> <span class="highlight">UP TO 1 BTC</span></h1>
<p class="hero-desc">+ 50 Free Spins on your first deposit. No wager limits.</p>
<div class="hero-actions">
<Button class="neon-button h-12 px-8 text-base font-bold">CLAIM NOW</Button>
<button class="text-link">Read Terms</button>
</div>
</div>
<div class="hero-image">
<div class="floating-coin">
<div class="coin-inner"></div>
</div>
</div>
</div>
<!-- Provider Filter Bar -->
<div class="providers-bar">
<div class="provider-list">
<button
class="provider-item"
:class="{ active: selectedProvider === 'All' }"
@click="selectedProvider = 'All'"
>All</button>
<button
v-for="p in providers"
:key="p"
class="provider-item"
:class="{ active: selectedProvider === p }"
@click="selectedProvider = p"
>{{ p }}</button>
</div>
</div>
<!-- BetiX Originals -->
<section class="game-section" id="originals">
<div class="section-header">
<h2>
BetiX <span class="handwritten">originals</span>
</h2>
<button class="view-all" @click="selectedProvider = 'All'">Show All</button>
</div>
<div v-if="originalsLoading" style="color:#888; font-weight:800; padding: 10px 4px;">{{ $t('dashboard.loading') }}</div>
<div v-else-if="originalsError" style="color:#ff8a8a; font-weight:800; padding: 10px 4px;">
{{ originalsError }}
<div style="margin-top:8px;">
<Button class="neon-button h-9 px-4" @click="loadOriginals">{{ $t('dashboard.reload') }}</Button>
</div>
</div>
<div v-else-if="filteredOriginals.length === 0" style="color:#777; font-weight:800; padding: 10px 4px;">{{ $t('dashboard.no_games') }}</div>
<div v-else class="game-grid">
<div v-for="game in filteredOriginals" :key="game.id" class="game-card group" @click="play(game)">
<div class="card-image">
<img :src="game.image || 'https://placehold.co/600x400?text=BetiX'" :alt="game.name" loading="lazy">
<div class="card-overlay">
<Button class="play-btn" @click.stop.prevent="play(game)"><PlayCircle class="w-8 h-8 text-white" /></Button>
<span class="provider">{{ game.provider || 'BetiX' }}</span>
</div>
<div v-if="game.tag" class="tag">{{ game.tag }}</div>
<div v-if="game.rtp" class="rtp-badge">RTP {{ game.rtp }}%</div>
<button class="fav-btn" :class="{ active: favorites.has(game.slug) }" @click.stop="toggleFavorite(game)" title="Favorite">
<Heart class="w-4 h-4" />
</button>
</div>
<div class="card-info">
<div class="game-name">{{ game.name }}</div>
</div>
</div>
</div>
</section>
<!-- Popular Slots horizontal slider -->
<section class="game-section" id="slots">
<div class="section-header">
<h2><Flame class="w-5 h-5 text-orange-500 inline mb-1" /> {{ $t('dashboard.popular_slots') }}</h2>
<button class="view-all" @click="selectedProvider = 'All'">{{ $t('dashboard.show_all') }}</button>
</div>
<div v-if="popularSlots.length === 0 && !gamesLoading" style="color:#777; font-weight:800; padding: 10px 4px;">{{ $t('dashboard.no_slots') }}</div>
<div class="slider-track">
<div v-for="slot in popularSlots" :key="slot.id" class="game-card slider-card group" @click="play(slot)">
<div class="card-image">
<img :src="slot.image || 'https://placehold.co/300x200?text=' + encodeURIComponent(slot.name)" :alt="slot.name" loading="lazy">
<div class="card-overlay">
<Button class="play-btn" @click.stop.prevent="play(slot)"><PlayCircle class="w-8 h-8 text-white" /></Button>
<span class="provider">{{ slot.provider }}</span>
</div>
<div v-if="slot.tag" class="tag">{{ slot.tag }}</div>
<div class="rtp-badge">RTP {{ slot.rtp }}%</div>
<button class="fav-btn" :class="{ active: favorites.has(slot.slug) }" @click.stop="toggleFavorite(slot)" title="Favorite">
<Heart class="w-4 h-4" />
</button>
</div>
<div class="card-info">
<div class="game-name">{{ slot.name }}</div>
</div>
</div>
</div>
</section>
<!-- Live Wins (Full Width) -->
<section class="wins-section">
<div class="section-header">
<h2><Trophy class="w-5 h-5 text-yellow-500 inline mb-1" /> {{ $t('dashboard.live_wins') }}</h2>
<div class="live-badge">
<span class="live-dot"></span>
{{ hasRealWins ? 'LIVE' : $t('dashboard.live_feed') }}
</div>
</div>
<div class="wins-grid">
<transition-group name="wins-list">
<div v-for="win in liveWins" :key="win.id" class="win-card">
<div class="win-icon">
<img :src="`https://api.dicebear.com/7.x/avataaars/svg?seed=${win.user}`" alt="Avatar" class="w-8 h-8 rounded-full bg-[#222]" />
</div>
<div class="win-details">
<div class="win-user">{{ maskName(win.user) }}</div>
<div class="win-game">{{ $t('dashboard.won_in') }} {{ win.game }}</div>
</div>
<div class="win-value">{{ win.amount }}</div>
</div>
</transition-group>
</div>
</section>
<!-- New Releases -->
<section class="game-section" id="new-releases">
<div class="section-header">
<h2><Star class="w-5 h-5 text-yellow-400 inline mb-1" /> {{ $t('dashboard.new_releases') }}</h2>
<button class="view-all" @click="selectedProvider = 'All'">{{ $t('dashboard.show_all') }}</button>
</div>
<div class="game-grid">
<div v-for="slot in newReleases" :key="slot.id" class="game-card group" @click="play(slot)">
<div class="card-image">
<img :src="slot.image" :alt="slot.name" loading="lazy">
<div class="card-overlay">
<Button class="play-btn" @click.stop.prevent="play(slot)"><PlayCircle class="w-8 h-8 text-white" /></Button>
<span class="provider">{{ slot.provider }}</span>
</div>
<div v-if="slot.tag" class="tag">{{ slot.tag }}</div>
</div>
<div class="card-info">
<div class="game-name">{{ slot.name }}</div>
</div>
</div>
</div>
</section>
<!-- Platform Features Strip -->
<div class="features-strip">
<div class="feature-item">
<div class="feature-icon"><Shield :size="28" /></div>
<div class="feature-label">Provably Fair</div>
<div class="feature-desc">Every result verifiable on-chain</div>
</div>
<div class="feature-item">
<div class="feature-icon"><Zap :size="28" /></div>
<div class="feature-label">Instant Payouts</div>
<div class="feature-desc">No waiting, no limits</div>
</div>
<div class="feature-item">
<div class="feature-icon"><Gamepad2 :size="28" /></div>
<div class="feature-label">Exclusive Originals</div>
<div class="feature-desc">Games only on BetiX</div>
</div>
<div class="feature-item">
<div class="feature-icon"><Star :size="28" /></div>
<div class="feature-label">VIP Rewards</div>
<div class="feature-desc">Climb ranks, earn more</div>
</div>
</div>
<!-- Live Casino -->
<section class="game-section" id="live">
<div class="section-header">
<h2><Zap class="w-5 h-5 text-blue-400 inline mb-1" /> {{ $t('dashboard.live_casino') }}</h2>
<button class="view-all" @click="selectedProvider = 'All'">{{ $t('dashboard.show_all') }}</button>
</div>
<div v-if="filteredLive.length === 0 && !gamesLoading" style="color:#777; font-weight:800; padding: 10px 4px;">{{ $t('dashboard.no_live') }}</div>
<div class="game-grid">
<div v-for="slot in filteredLive" :key="slot.id" class="game-card group" @click="play(slot)">
<div class="card-image">
<img :src="slot.image" :alt="slot.name" loading="lazy">
<div class="card-overlay">
<Button class="play-btn" @click.stop.prevent="play(slot)"><PlayCircle class="w-8 h-8 text-white" /></Button>
<span class="provider">{{ slot.provider }}</span>
</div>
<div v-if="slot.tag" class="tag">{{ slot.tag }}</div>
</div>
<div class="card-info">
<div class="game-name">{{ slot.name }}</div>
</div>
</div>
</div>
</section>
<!-- All Time Best Hits (Hall of Fame) -->
<section class="hits-section">
<div class="section-header">
<h2><Trophy class="w-5 h-5 text-purple-500 inline mb-1" /> {{ $t('dashboard.hall_of_fame') }}</h2>
</div>
<div class="hits-card">
<div v-if="hallOfFame.length === 0" style="color:#555; padding: 24px; text-align:center;">No wins recorded yet.</div>
<div v-else class="hits-container">
<!-- Rank 1 Hero -->
<div class="hit-hero">
<div class="hit-hero-image">
<img
:src="hallOfFame[0].image || 'https://placehold.co/600x300?text=' + encodeURIComponent(hallOfFame[0].game)"
:alt="hallOfFame[0].game"
>
<div class="hit-hero-overlay">
<div class="rank-badge rank-1">#1</div>
<div class="hit-hero-content">
<div class="hit-hero-multi">{{ hallOfFame[0].multiplier }}</div>
<div class="hit-hero-amount">{{ hallOfFame[0].amount }}</div>
<div class="hit-hero-user">by {{ maskName(hallOfFame[0].user) }}</div>
</div>
</div>
</div>
</div>
<!-- Rank 2-5 List -->
<div class="hits-list">
<div v-for="hit in hallOfFame.slice(1)" :key="hit.id" class="hit-item">
<div class="hit-rank">{{ hit.rank }}</div>
<div class="hit-thumb">
<img :src="hit.image || 'https://placehold.co/80x60?text=' + encodeURIComponent(hit.game)" :alt="hit.game">
</div>
<div class="hit-details">
<div class="hit-multi">{{ hit.multiplier }}</div>
<div class="hit-game-name">{{ hit.game }}</div>
</div>
<div class="hit-amount-small">{{ hit.amount }}</div>
</div>
</div>
</div>
</div>
</section>
<!-- Recently Played -->
<section v-if="recentlyPlayed.length > 0" class="game-section">
<div class="section-header">
<h2>🕒 {{ $t('dashboard.recently_played') }}</h2>
</div>
<div class="recent-grid">
<div
v-for="g in recentlyPlayed"
:key="g.game_name"
class="recent-card"
@click="play({ name: g.game_name, slug: g.slug })"
>
<div class="recent-name">{{ g.game_name }}</div>
<div class="recent-date">{{ new Date(g.last_played_at).toLocaleDateString('de-DE') }}</div>
</div>
</div>
</section>
<!-- FAQ -->
<section class="faq-section">
<div class="section-header">
<h2><HelpCircle class="w-5 h-5 text-gray-400 inline mb-1" /> FAQ</h2>
</div>
<div class="faq-list">
<div v-for="(faq, index) in faqs" :key="index" class="faq-item" :class="{ active: activeFaq === index }">
<div class="faq-question" @click="toggleFaq(index)">
{{ faq.q }}
<ChevronDown class="w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': activeFaq === index }" />
</div>
<div class="faq-answer" :class="{ open: activeFaq === index }">
<div class="faq-content">
{{ faq.a }}
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Game Loading Overlay -->
<Teleport to="body">
<div v-if="gameLoading" class="game-launch-overlay">
<div class="game-launch-spinner"></div>
</div>
<div v-if="gameSession" class="game-overlay" @keydown.esc="gameSession = null" tabindex="-1">
<div class="game-overlay-inner">
<div class="game-overlay-header">
<span class="game-overlay-title">{{ gameSession.game }}</span>
<button class="game-overlay-close" @click="gameSession = null" title="Close"></button>
</div>
<iframe
:src="gameSession.url"
class="game-iframe"
allowfullscreen
allow="autoplay"
frameborder="0"
></iframe>
</div>
</div>
</Teleport>
</UserLayout>
</template>
<style scoped>
.dashboard-content {
padding: 30px;
max-width: 1600px;
margin: 0 auto;
}
/* Hero Banner */
.hero-banner {
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
border: 1px solid #151515;
border-radius: 24px;
padding: 60px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
margin-bottom: 40px;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.hero-banner::before {
content: '';
position: absolute;
top: -50%;
left: -20%;
width: 80%;
height: 200%;
background: radial-gradient(circle, rgba(223, 0, 106, 0.15) 0%, transparent 70%);
pointer-events: none;
}
.hero-content { z-index: 2; max-width: 500px; }
.badge { background: rgba(223, 0, 106, 0.1); color: var(--primary); padding: 6px 12px; border-radius: 50px; font-size: 12px; font-weight: 800; display: inline-block; margin-bottom: 20px; border: 1px solid rgba(223, 0, 106, 0.3); }
.hero-title { font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin-bottom: 15px; color: white; }
.highlight { color: #00f2ff; text-shadow: 0 0 20px rgba(0,242,255,0.4); }
.hero-desc { color: #888; font-size: 1.1rem; margin-bottom: 30px; }
.hero-actions { display: flex; gap: 20px; align-items: center; }
.text-link { color: #666; font-size: 14px; font-weight: 600; text-decoration: underline; cursor: pointer; transition: color 0.2s; }
.text-link:hover { color: white; }
/* Floating Coin */
.floating-coin { width: 200px; height: 200px; background: linear-gradient(45deg, #ffb700, #ff8800); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 50px rgba(255, 136, 0, 0.4), inset 0 0 20px rgba(255,255,255,0.5); animation: float 6s ease-in-out infinite; position: relative; z-index: 2; }
.coin-inner { font-size: 100px; font-weight: 900; color: rgba(255,255,255,0.9); text-shadow: 0 2px 10px rgba(0,0,0,0.2); }
@keyframes float { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-20px) rotate(5deg); } }
/* Providers Bar */
.providers-bar {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 40px;
background: #0a0a0a;
border: 1px solid #151515;
padding: 10px 20px;
border-radius: 12px;
}
.provider-list {
flex: 1;
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 5px;
scrollbar-width: none;
}
.provider-list::-webkit-scrollbar { display: none; }
.provider-item {
background: #111;
border: 1px solid #222;
color: #888;
padding: 8px 16px;
border-radius: 8px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
}
.provider-item:hover { color: white; border-color: #444; }
.provider-item.active { background: #fff; color: black; border-color: #fff; }
.filter-btn {
display: flex;
align-items: center;
gap: 8px;
background: #151515;
border: 1px solid #222;
color: white;
padding: 8px 16px;
border-radius: 8px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
/* Horizontal Slider */
.slider-track {
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 10px;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
scrollbar-color: #222 transparent;
}
.slider-track::-webkit-scrollbar { height: 4px; }
.slider-track::-webkit-scrollbar-track { background: transparent; }
.slider-track::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 2px; }
.slider-card {
flex: 0 0 180px;
scroll-snap-align: start;
}
/* Live badge */
.live-badge {
display: flex; align-items: center; gap: 6px;
font-size: 11px; font-weight: 800; text-transform: uppercase;
color: var(--primary); letter-spacing: 1px;
}
.live-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--primary);
animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.75); } }
/* Features Strip */
.features-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 50px;
background: #070709;
border: 1px solid #151515;
border-radius: 20px;
padding: 32px 24px;
}
.feature-item {
display: flex; flex-direction: column; align-items: center;
text-align: center; gap: 10px;
}
.feature-icon {
width: 56px; height: 56px; border-radius: 16px;
background: rgba(223,0,106,.08); border: 1px solid rgba(223,0,106,.15);
display: flex; align-items: center; justify-content: center;
color: var(--primary);
}
.feature-label { font-size: 13px; font-weight: 800; color: #fff; }
.feature-desc { font-size: 11px; color: #555; font-weight: 600; line-height: 1.4; }
/* Game Grid */
.game-section { margin-bottom: 50px; }
.section-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; }
.section-header h2 { font-size: 18px; font-weight: 800; color: white; text-transform: uppercase; letter-spacing: 1px; margin: 0; }
.view-all { font-size: 12px; color: var(--primary); font-weight: 700; text-transform: uppercase; background: none; border: none; cursor: pointer; padding: 0; text-decoration: none; }
.view-all:hover { opacity: 0.75; }
/* Handwritten style for 'originals' word */
.handwritten {
font-family: 'Caveat', 'Pacifico', 'Brush Script MT', cursive;
font-weight: 900;
font-style: italic;
letter-spacing: 1px;
color: #fff;
}
.game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
.game-card {
background: #0a0a0a;
border-radius: 16px;
overflow: hidden;
border: 1px solid #151515;
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1), box-shadow 0.3s, border-color 0.3s;
cursor: pointer;
position: relative;
}
.game-card:hover {
transform: translateY(-6px) scale(1.01);
box-shadow: 0 20px 50px rgba(0,0,0,0.7), 0 0 0 1px rgba(223, 0, 106, 0.25), 0 0 24px rgba(223, 0, 106, 0.12);
border-color: rgba(223, 0, 106, 0.3);
z-index: 2;
}
.card-image { position: relative; aspect-ratio: 4/3; overflow: hidden; }
.card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s cubic-bezier(0.2, 0, 0, 1); }
.game-card:hover img { transform: scale(1.08); filter: brightness(0.35) saturate(0.6); }
.card-overlay {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.3s;
background: rgba(5, 5, 5, 0.45);
backdrop-filter: blur(3px) saturate(1.2);
}
.game-card:hover .card-overlay { opacity: 1; }
.play-btn {
width: 60px; height: 60px; border-radius: 50%;
background: rgba(255,255,255,0.08);
backdrop-filter: blur(8px);
border: 1.5px solid rgba(255,255,255,0.2);
color: white; display: flex; align-items: center; justify-content: center;
margin-bottom: 10px; transform: scale(0.75);
transition: transform 0.25s cubic-bezier(0.2, 0, 0, 1), background 0.2s, border-color 0.2s;
padding: 0;
box-shadow: 0 0 20px rgba(223, 0, 106, 0.2);
}
.game-card:hover .play-btn { transform: scale(1); }
.play-btn:hover {
background: rgba(223, 0, 106, 0.3);
border-color: rgba(223, 0, 106, 0.6);
box-shadow: 0 0 30px rgba(223, 0, 106, 0.4);
}
.provider {
font-size: 10px; font-weight: 800; color: rgba(255,255,255,0.9);
text-transform: uppercase; letter-spacing: 1.5px;
background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
padding: 3px 8px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1);
}
.tag {
position: absolute; top: 10px; left: 10px;
padding: 3px 8px; border-radius: 5px;
font-size: 9px; font-weight: 900; text-transform: uppercase;
z-index: 2; background: var(--primary); color: white;
box-shadow: 0 2px 10px rgba(223, 0, 106, 0.4);
letter-spacing: 0.5px;
}
.rtp-badge {
position: absolute; bottom: 10px; right: 10px;
background: rgba(0,0,0,0.75); backdrop-filter: blur(4px);
color: #bbb; padding: 2px 7px; border-radius: 5px;
font-size: 9px; font-weight: 700;
border: 1px solid rgba(255,255,255,0.1);
}
.card-info { padding: 12px; background: #0a0a0a; border-top: 1px solid #151515; }
.game-name { font-size: 13px; font-weight: 700; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Wins Section */
.wins-section { margin-bottom: 50px; }
.wins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 15px; }
.win-card {
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(6px);
border: 1px solid #1a1a1a;
border-radius: 12px; padding: 12px 15px;
display: flex; align-items: center; gap: 12px;
transition: all 0.25s cubic-bezier(0.2, 0, 0, 1);
}
.win-card:hover {
border-color: rgba(223, 0, 106, 0.25);
background: rgba(18, 5, 12, 0.9);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.5), 0 0 12px rgba(223,0,106,0.06);
}
.win-icon { flex-shrink: 0; }
.win-details { flex: 1; overflow: hidden; }
.win-user { font-size: 13px; font-weight: 700; color: #fff; }
.win-game { font-size: 11px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.win-value { font-size: 13px; font-weight: 800; color: #00ff9d; text-align: right; }
/* Hits Section (Hall of Fame) */
.hits-section { margin-bottom: 50px; }
.hits-card {
background: #0a0a0a;
border: 1px solid #151515;
border-radius: 20px;
padding: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
.hits-container {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 20px;
}
.hit-hero {
position: relative;
border-radius: 16px;
overflow: hidden;
border: 1px solid #ffb700;
box-shadow: 0 0 30px rgba(255, 183, 0, 0.15);
height: 220px; /* Reduced height */
}
.hit-hero-image {
width: 100%;
height: 100%;
position: relative;
}
.hit-hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s;
}
.hit-hero:hover img { transform: scale(1.05); }
.hit-hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.95), transparent);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
}
.rank-badge {
position: absolute; top: 15px; left: 15px;
background: linear-gradient(135deg, #ffb700, #ff8800);
color: black;
font-size: 14px; font-weight: 900;
padding: 6px 12px; border-radius: 6px;
box-shadow: 0 0 20px rgba(255, 183, 0, 0.6);
}
.hit-hero-multi { font-size: 36px; font-weight: 900; color: #ffb700; text-shadow: 0 0 20px rgba(255, 183, 0, 0.4); line-height: 1; margin-bottom: 8px; }
.hit-hero-amount { font-size: 18px; font-weight: 800; color: #00ff9d; margin-bottom: 4px; }
.hit-hero-user { font-size: 12px; color: #ccc; font-weight: 600; }
.hits-list {
display: flex;
flex-direction: column;
gap: 8px; /* Reduced gap */
}
.hit-item {
display: flex;
align-items: center;
gap: 12px;
background: #111;
border: 1px solid #222;
padding: 8px 12px; /* Reduced padding */
border-radius: 10px;
transition: all 0.3s;
}
.hit-item:hover {
border-color: #444;
background: #151515;
transform: translateX(5px);
}
.hit-rank {
font-size: 14px; font-weight: 900; color: #444; width: 25px; text-align: center;
}
.hit-thumb {
width: 40px; height: 30px; border-radius: 6px; overflow: hidden;
}
.hit-thumb img { width: 100%; height: 100%; object-fit: cover; }
.hit-details { flex: 1; }
.hit-multi { font-size: 13px; font-weight: 800; color: #ffb700; }
.hit-game-name { font-size: 10px; color: #666; font-weight: 600; }
.hit-amount-small { font-size: 12px; font-weight: 800; color: #00ff9d; }
/* FAQ */
.faq-section { margin-bottom: 50px; max-width: 800px; margin-left: auto; margin-right: auto; }
.faq-item { background: #0a0a0a; border: 1px solid #151515; border-radius: 12px; margin-bottom: 10px; overflow: hidden; }
.faq-item.active { border-color: #333; }
.faq-question { padding: 15px 20px; font-size: 14px; font-weight: 700; color: #ccc; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.faq-question:hover { color: white; }
.faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.faq-answer.open {
max-height: 200px; /* Adjust based on content */
}
.faq-content {
padding: 0 20px 20px; font-size: 13px; color: #888; line-height: 1.5;
}
/* Neon Button */
.neon-button { background: linear-gradient(90deg, var(--primary), #a3004d); border: none; position: relative; overflow: hidden; transition: all 0.3s ease; color: white; }
.neon-button:hover { transform: translateY(-2px); box-shadow: 0 0 30px rgba(223, 0, 106, 0.6); filter: brightness(1.1); }
@media (max-width: 1000px) {
.dashboard-content { padding: 12px; }
.hero-banner {
flex-direction: column;
padding: 24px;
min-height: auto;
gap: 20px;
text-align: center;
}
.hero-content { padding: 0; }
.hero-title { font-size: 28px; }
.hero-desc { font-size: 14px; }
.hero-image { display: none; }
.hero-actions { justify-content: center; }
.providers-bar {
padding: 10px;
margin-bottom: 20px;
position: sticky;
top: 64px;
z-index: 800;
background: rgba(5,5,5,0.9);
backdrop-filter: blur(10px);
border-radius: 0;
margin: -12px -12px 20px -12px;
}
.provider-list { gap: 8px; }
.provider-item { padding: 6px 12px; font-size: 11px; }
.features-strip { grid-template-columns: repeat(2, 1fr); }
.game-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.section-header h2 { font-size: 16px; }
.game-name { font-size: 12px; }
.wins-grid { grid-template-columns: 1fr; }
.hits-container { grid-template-columns: 1fr; }
.hit-hero { height: 250px; }
.footer-grid { grid-template-columns: 1fr; text-align: center; gap: 30px; }
}
@media (max-width: 480px) {
.hero-title { font-size: 24px; }
.game-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
.card-info { padding: 8px; }
.features-strip { grid-template-columns: repeat(2, 1fr); padding: 20px 16px; gap: 12px; }
.slider-card { flex: 0 0 150px; }
.recent-grid { gap: 8px; }
.recent-card { min-width: calc(50% - 4px); }
}
/* Favorite Button */
.fav-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0,0,0,0.6);
border: 1px solid #333;
border-radius: 8px;
padding: 5px;
cursor: pointer;
color: #666;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s, background 0.2s, border-color 0.2s;
z-index: 10;
}
.fav-btn:hover { color: #ff4d80; border-color: #ff4d80; background: rgba(255,77,128,0.12); }
.fav-btn.active { color: #ff4d80; border-color: #ff4d80; background: rgba(255,77,128,0.15); }
/* Recently Played */
.recent-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.recent-card {
background: #0d0d0d;
border: 1px solid #1a1a1a;
border-radius: 12px;
padding: 14px 20px;
cursor: pointer;
transition: border-color 0.2s, transform 0.15s;
min-width: 140px;
}
.recent-card:hover {
border-color: rgba(223, 0, 106, 0.5);
transform: translateY(-2px);
}
.recent-name {
font-size: 13px;
font-weight: 800;
color: #fff;
margin-bottom: 4px;
}
.recent-date {
font-size: 11px;
color: #555;
}
/* Game Launch Loading Overlay */
.game-launch-overlay {
position: fixed; inset: 0; z-index: 9000;
background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
}
.game-launch-spinner {
width: 48px; height: 48px; border-radius: 50%;
border: 3px solid #222;
border-top-color: var(--primary);
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Iframe Game Overlay */
.game-overlay {
position: fixed; inset: 0; z-index: 9001;
background: rgba(0,0,0,0.92);
display: flex; align-items: center; justify-content: center;
padding: 16px;
}
.game-overlay-inner {
width: 100%; max-width: 1200px; height: 90vh;
display: flex; flex-direction: column;
background: #0a0a0a;
border-radius: 16px;
border: 1px solid #222;
overflow: hidden;
box-shadow: 0 0 80px rgba(0,0,0,0.8);
}
.game-overlay-header {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 20px;
background: #111;
border-bottom: 1px solid #1a1a1a;
flex-shrink: 0;
}
.game-overlay-title {
font-size: 15px; font-weight: 800; color: #fff; text-transform: uppercase; letter-spacing: 1px;
}
.game-overlay-close {
width: 32px; height: 32px;
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
color: #888; font-size: 16px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: color 0.2s, border-color 0.2s;
}
.game-overlay-close:hover { color: #fff; border-color: var(--primary); }
.game-iframe {
flex: 1; width: 100%; border: none;
}
@media (max-width: 600px) {
.game-overlay { padding: 0; }
.game-overlay-inner { border-radius: 0; height: 100vh; max-width: 100%; }
}
</style>