1561 lines
54 KiB
Vue
1561 lines
54 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||
import { Head, router } from '@inertiajs/vue3';
|
||
import { Maximize, MonitorPlay, BarChart2, RefreshCw, ArrowLeft, Trophy, Info, PlayCircle, ShieldCheck, Heart, Crosshair, Sparkles, Star } from 'lucide-vue-next';
|
||
import Button from '@/components/ui/button.vue';
|
||
import UserLayout from '@/layouts/user/userlayout.vue';
|
||
|
||
// Props from Inertia route
|
||
const props = defineProps<{
|
||
title?: string;
|
||
slug?: string | null;
|
||
src?: string | null;
|
||
provider?: string | null;
|
||
description?: string | null;
|
||
}>();
|
||
|
||
const loading = ref(true);
|
||
const error = ref<string | null>(null);
|
||
|
||
// UI State
|
||
const isCinema = ref<boolean>(false);
|
||
const isFullscreen = ref<boolean>(false);
|
||
const gameCardRef = ref<HTMLElement | null>(null);
|
||
|
||
try { isCinema.value = sessionStorage.getItem('gp:isCinema') === '1'; } catch {}
|
||
const showStats = ref(false);
|
||
const modeKey = computed(() => `gp:mode:${props.slug ?? 'default'}`);
|
||
const mode = ref<'real' | 'demo'>('demo');
|
||
|
||
const titleText = computed(() => props.title || props.slug || 'Game');
|
||
const providerText = computed(() => props.provider || 'Unknown');
|
||
|
||
// ---- Game Meta (fallbacks) ----
|
||
const fallbackMeta: Record<string, { description: string; provider?: string; rtp?: number; volatility?: 'low'|'medium'|'high'; image?: string; category?: string; }> = {
|
||
'gates-of-olympus': { description: 'Stürze dich mit Zeus in stürmische Multiplikatoren und Freispiele.', provider: 'Pragmatic Play', rtp: 96.5, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/pragmatic/GatesOfOlympus.png', category: 'mythology' },
|
||
'sweet-bonanza': { description: 'Süße Gewinne mit Bonbons und Kaskaden-Features.', provider: 'Pragmatic Play', rtp: 96.48, volatility: 'medium', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SweetBonanza.png', category: 'fruits' },
|
||
'razor-shark': { description: 'Tauche ab nach Schätzen mit Mystery-Stacks und Multiplikatoren.', provider: 'Push Gaming', rtp: 96.7, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/pushgaming/RazorShark.png', category: 'ocean' },
|
||
'mental': { description: 'Extrem volatile Irrenanstalt – nichts für schwache Nerven.', provider: 'Nolimit City', rtp: 96.08, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/nolimit/Mental.png', category: 'horror' },
|
||
'wanted-dead-or-a-wild': { description: 'Western‑Action mit massiven Multiplikatoren und Bonuskäufen.', provider: 'Hacksaw', rtp: 96.38, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/hacksaw/WantedDeadOrAWild.png', category: 'western' },
|
||
};
|
||
|
||
const slug = computed(() => String(props.slug ?? '').toLowerCase());
|
||
const gameMeta = computed(() => fallbackMeta[slug.value] || null);
|
||
const descriptionText = computed(() => props.description || gameMeta.value?.description || 'Erlebe spannende Action und unglaubliche Gewinnchancen in diesem Top-Hit!');
|
||
|
||
// Players online (MVP mock: random walk)
|
||
const playerCount = ref<number>(Math.floor(80 + Math.random()*140));
|
||
let presenceTimer: number | undefined;
|
||
|
||
// Live winners (MVP mock)
|
||
const liveWins = ref([
|
||
{ id: 1, user: 'CryptoKing', game: titleText.value, amount: '0.05 BTC', isWin: true },
|
||
{ id: 2, user: 'LuckyLuke', game: titleText.value, amount: '1.2 ETH', isWin: true },
|
||
{ id: 3, user: 'Anna_99', game: titleText.value, amount: '500 USDT', isWin: true },
|
||
{ id: 4, user: 'Satoshi', game: titleText.value, amount: '0.01 BTC', isWin: true },
|
||
{ id: 5, user: 'Whale_Hunter', game: titleText.value, amount: '2.5 ETH', isWin: true },
|
||
{ id: 6, user: 'Dolo', game: titleText.value, amount: '1500 XRP', isWin: true },
|
||
]);
|
||
let winnersNextId = 7;
|
||
let winnersTimer: number | undefined;
|
||
|
||
function maskName(name: string) {
|
||
if (!name) return '';
|
||
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, '');
|
||
|
||
// Similar games (simple heuristic using local list)
|
||
const catalog = [
|
||
{ name: 'Gates of Olympus', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/GatesOfOlympus.png' },
|
||
{ name: 'Sweet Bonanza', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SweetBonanza.png' },
|
||
{ name: 'Wanted Dead or a Wild', provider: 'Hacksaw', image: 'https://cdn.softswiss.net/i/s3/hacksaw/WantedDeadOrAWild.png' },
|
||
{ name: 'Razor Shark', provider: 'Push Gaming', image: 'https://cdn.softswiss.net/i/s3/pushgaming/RazorShark.png' },
|
||
{ name: 'Mental', provider: 'Nolimit City', image: 'https://cdn.softswiss.net/i/s3/nolimit/Mental.png' },
|
||
{ name: 'Sugar Rush 1000', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SugarRush.png' },
|
||
{ name: 'Le Bandit', provider: 'Hacksaw', image: 'https://cdn.softswiss.net/i/s3/hacksaw/LeBandit.png' },
|
||
{ name: 'Big Bamboo', provider: 'Push Gaming', image: 'https://cdn.softswiss.net/i/s3/pushgaming/BigBamboo.png' },
|
||
];
|
||
|
||
const similar = computed(() => {
|
||
const baseProv = providerText.value;
|
||
const current = slug.value;
|
||
const arr = catalog
|
||
.filter(g => g.provider === baseProv)
|
||
.filter(g => slugify(g.name) !== current)
|
||
.slice(0, 4)
|
||
.map(g => ({ ...g, slug: slugify(g.name) }));
|
||
|
||
if (arr.length < 4) {
|
||
const more = catalog.filter(g => slugify(g.name) !== current && !arr.find(a => a.name === g.name)).slice(0, 4 - arr.length);
|
||
arr.push(...more.map(g => ({ ...g, slug: slugify(g.name) })));
|
||
}
|
||
return arr;
|
||
});
|
||
|
||
function playGameBySlug(s: string, prov?: string) {
|
||
if (!s) return;
|
||
const providerSegment = encodeURIComponent((prov || providerText.value || 'betix').toLowerCase().replace(/\s+/g, '-'));
|
||
router.visit(`/games/play/${providerSegment}/${encodeURIComponent(s)}`);
|
||
}
|
||
|
||
const BETIX_ORIGINALS = ['dice', 'crash', 'mines', 'plinko'];
|
||
const isOriginal = computed(() => BETIX_ORIGINALS.includes(slug.value));
|
||
const originalLaunchUrl = ref<string | null>(null);
|
||
const originalLoading = ref(false);
|
||
|
||
async function launchOriginalSession() {
|
||
if (!isOriginal.value) return;
|
||
originalLoading.value = true;
|
||
loading.value = true;
|
||
try {
|
||
const csrf = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '';
|
||
const res = await fetch('/api/originals/launch', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
|
||
body: JSON.stringify({ game: slug.value }),
|
||
signal: AbortSignal.timeout(20000),
|
||
});
|
||
if (!res.ok) throw new Error(`${res.status}`);
|
||
const data = await res.json();
|
||
originalLaunchUrl.value = data.launch_url;
|
||
loading.value = false;
|
||
} catch (e) {
|
||
console.error('[GamePlay] launchOriginalSession failed:', e);
|
||
error.value = 'Spiel konnte nicht gestartet werden.';
|
||
loading.value = false;
|
||
} finally {
|
||
originalLoading.value = false;
|
||
}
|
||
}
|
||
|
||
const iframeSrc = computed(() => {
|
||
if (isOriginal.value) {
|
||
return originalLaunchUrl.value ?? '';
|
||
}
|
||
if (props.slug) {
|
||
const q = `?mode=${encodeURIComponent(mode.value)}`;
|
||
return `/games/embed/${encodeURIComponent(String(props.slug))}${q}`;
|
||
}
|
||
return '';
|
||
});
|
||
|
||
function onLoad() {
|
||
loading.value = false;
|
||
}
|
||
|
||
function onError() {
|
||
loading.value = false;
|
||
error.value = 'Spiel konnte nicht geladen werden.';
|
||
}
|
||
|
||
function goBack() {
|
||
if (window.history.length > 1) {
|
||
window.history.back();
|
||
} else {
|
||
router.visit('/dashboard');
|
||
}
|
||
}
|
||
|
||
function reload() {
|
||
loading.value = true;
|
||
const el = document.getElementById('game-frame') as HTMLIFrameElement | null;
|
||
if (el) el.src = iframeSrc.value;
|
||
}
|
||
|
||
function scrollToCenter() {
|
||
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
|
||
function toggleCinema() {
|
||
if (isFullscreen.value) toggleFullscreen();
|
||
isCinema.value = !isCinema.value;
|
||
try { sessionStorage.setItem('gp:isCinema', isCinema.value ? '1' : '0'); } catch {}
|
||
|
||
if (isCinema.value) {
|
||
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
|
||
document.dispatchEvent(new CustomEvent('hide-support-chat'));
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}, 300);
|
||
});
|
||
} else {
|
||
isAmbiente.value = false;
|
||
document.dispatchEvent(new CustomEvent('expand-sidebar'));
|
||
document.dispatchEvent(new CustomEvent('show-support-chat'));
|
||
}
|
||
}
|
||
|
||
function selectCinema() {
|
||
showCinemaMenu.value = false;
|
||
if (isAmbiente.value) {
|
||
isAmbiente.value = false;
|
||
return;
|
||
}
|
||
toggleCinema();
|
||
}
|
||
|
||
function selectAmbiente() {
|
||
showCinemaMenu.value = false;
|
||
if (isAmbiente.value) {
|
||
isAmbiente.value = false;
|
||
return;
|
||
}
|
||
if (!isCinema.value) {
|
||
isCinema.value = true;
|
||
try { sessionStorage.setItem('gp:isCinema', '1'); } catch {}
|
||
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
|
||
document.dispatchEvent(new CustomEvent('hide-support-chat'));
|
||
}
|
||
isAmbiente.value = true;
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}, 300);
|
||
});
|
||
}
|
||
|
||
function toggleFullscreen() {
|
||
if (!document.fullscreenElement) {
|
||
if (isCinema.value) {
|
||
isCinema.value = false;
|
||
try { sessionStorage.setItem('gp:isCinema', '0'); } catch {}
|
||
}
|
||
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
|
||
document.dispatchEvent(new CustomEvent('hide-support-chat'));
|
||
gameCardRef.value?.requestFullscreen().catch(err => {
|
||
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||
});
|
||
} else {
|
||
document.exitFullscreen();
|
||
document.dispatchEvent(new CustomEvent('expand-sidebar'));
|
||
}
|
||
}
|
||
|
||
function handleFullscreenChange() {
|
||
isFullscreen.value = !!document.fullscreenElement;
|
||
if (!isFullscreen.value) {
|
||
if (!isCinema.value) {
|
||
document.dispatchEvent(new CustomEvent('expand-sidebar'));
|
||
}
|
||
document.dispatchEvent(new CustomEvent('show-support-chat'));
|
||
}
|
||
}
|
||
|
||
function toggleStats() {
|
||
showStats.value = !showStats.value;
|
||
}
|
||
|
||
function setMode(next: 'real' | 'demo') {
|
||
if (mode.value === next) return;
|
||
mode.value = next;
|
||
try { localStorage.setItem(modeKey.value, next); } catch {}
|
||
if (props.slug) {
|
||
reload();
|
||
}
|
||
}
|
||
|
||
function onKeydown(e: KeyboardEvent) {
|
||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||
|
||
if (e.key.toLowerCase() === 'c') {
|
||
e.preventDefault();
|
||
toggleCinema();
|
||
} else if (e.key.toLowerCase() === 'f') {
|
||
e.preventDefault();
|
||
toggleFullscreen();
|
||
} else if (e.key === 'Escape') {
|
||
if (showStats.value) showStats.value = false;
|
||
}
|
||
}
|
||
|
||
const isFavorite = ref(false);
|
||
function toggleFavorite() {
|
||
isFavorite.value = !isFavorite.value;
|
||
}
|
||
|
||
const showInfo = ref(false);
|
||
const isAmbiente = ref(false);
|
||
const showCinemaMenu = ref(false);
|
||
const cinemaMenuRef = ref<HTMLElement | null>(null);
|
||
|
||
const CURRENCIES = [
|
||
{ symbol: 'BTX', name: 'BetiX Coin', color: '#ff007a' },
|
||
{ symbol: 'BTC', name: 'Bitcoin', color: '#f7931a' },
|
||
{ symbol: 'ETH', name: 'Ethereum', color: '#627eea' },
|
||
{ symbol: 'SOL', name: 'Solana', color: '#9945ff' },
|
||
{ symbol: 'USDT', name: 'Tether', color: '#26a17b' },
|
||
];
|
||
const selectedCurrency = ref('BTX');
|
||
const showCurrencyMenu = ref(false);
|
||
const currencyMenuRef = ref<HTMLElement | null>(null);
|
||
|
||
function handleCinemaMenuOutside(e: MouseEvent) {
|
||
if (showCinemaMenu.value && cinemaMenuRef.value && !cinemaMenuRef.value.contains(e.target as Node)) {
|
||
showCinemaMenu.value = false;
|
||
}
|
||
if (showCurrencyMenu.value && currencyMenuRef.value && !currencyMenuRef.value.contains(e.target as Node)) {
|
||
showCurrencyMenu.value = false;
|
||
}
|
||
}
|
||
|
||
// Trigger balance refresh in UserLayout when the game iframe signals a round completed.
|
||
// The BetiX game server may postMessage after processing a round (e.g. { type: 'betix:round' }).
|
||
// We also listen for any postMessage from the game origin and schedule a refresh,
|
||
// because the round webhook fires asynchronously — give it a short delay.
|
||
let roundRefreshTimer: number | undefined;
|
||
function onGameMessage(e: MessageEvent) {
|
||
// Only react to messages from our game server (loose check for local dev)
|
||
if (typeof e.data !== 'object' || !e.data) return;
|
||
const type: string = e.data.type ?? '';
|
||
if (type.startsWith('betix:') || type === 'casino.embed.ready' || type === 'round' || type === 'result') {
|
||
// Debounce: cancel previous timer, wait ~1.5 s for the webhook to land before polling
|
||
clearTimeout(roundRefreshTimer);
|
||
roundRefreshTimer = window.setTimeout(() => {
|
||
document.dispatchEvent(new CustomEvent('balance:refresh'));
|
||
}, 1500);
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('keydown', onKeydown);
|
||
window.addEventListener('message', onGameMessage);
|
||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||
document.addEventListener('click', handleCinemaMenuOutside);
|
||
|
||
try {
|
||
const saved = localStorage.getItem(modeKey.value);
|
||
if (saved === 'real' || saved === 'demo') mode.value = saved;
|
||
} catch {}
|
||
|
||
// BetiX Originals always launch in real-money mode via session
|
||
if (isOriginal.value) {
|
||
mode.value = 'real';
|
||
launchOriginalSession();
|
||
}
|
||
|
||
presenceTimer = window.setInterval(() => {
|
||
const delta = Math.floor((Math.random() - 0.5) * 6);
|
||
playerCount.value = Math.max(1, playerCount.value + delta);
|
||
}, 3000);
|
||
|
||
winnersTimer = window.setInterval(() => {
|
||
const users = ['Andri_X', 'CryptoKing', 'Neon_Ripper', 'Satoshi', 'Whale_Hunter', 'Dolo'];
|
||
const amounts = ['0.01 BTC', '0.5 ETH', '200 USDT', '1500 XRP', '0.2 BTC'];
|
||
|
||
const newWin = {
|
||
id: winnersNextId++,
|
||
user: users[Math.floor(Math.random() * users.length)],
|
||
game: titleText.value,
|
||
amount: amounts[Math.floor(Math.random() * amounts.length)],
|
||
isWin: true
|
||
};
|
||
|
||
liveWins.value.unshift(newWin);
|
||
if (liveWins.value.length > 6) liveWins.value.pop();
|
||
}, 2500);
|
||
|
||
if (isCinema.value) {
|
||
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
|
||
// SupportChat may mount after GamePlay, defer so its listener is ready
|
||
nextTick(() => {
|
||
document.dispatchEvent(new CustomEvent('hide-support-chat'));
|
||
});
|
||
}
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('keydown', onKeydown);
|
||
window.removeEventListener('message', onGameMessage);
|
||
clearTimeout(roundRefreshTimer);
|
||
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||
document.removeEventListener('click', handleCinemaMenuOutside);
|
||
if (presenceTimer) clearInterval(presenceTimer);
|
||
if (winnersTimer) clearInterval(winnersTimer);
|
||
// Restore support icon when navigating away from game page
|
||
document.dispatchEvent(new CustomEvent('show-support-chat'));
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<UserLayout>
|
||
<Head :title="titleText" />
|
||
|
||
<!-- Ambiente Background Glow -->
|
||
<Teleport to="body">
|
||
<div v-if="isAmbiente" class="ambient-bg" />
|
||
</Teleport>
|
||
<div class="gameplay-page" :class="{ 'cinema-active': isCinema, 'fullscreen-active': isFullscreen, 'ambiente-active': isAmbiente }">
|
||
<!-- Full width Top Nav -->
|
||
<div class="top-nav" v-show="!isCinema && !isFullscreen">
|
||
<button class="back-link" @click="goBack">
|
||
<ArrowLeft class="w-4 h-4 mr-2" /> Zurück zur Lobby
|
||
</button>
|
||
<div class="game-path">
|
||
<span>Casino</span> <span class="sep">/</span>
|
||
<span>{{ providerText }}</span> <span class="sep">/</span>
|
||
<span class="current">{{ titleText }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- The Game Player (Takes full width alone) -->
|
||
<div class="game-card" ref="gameCardRef" :class="{ cinema: isCinema, fullscreen: isFullscreen, ambiente: isAmbiente }">
|
||
|
||
<!-- Cinema/Fullscreen Close Button (Top Right) -->
|
||
<button v-if="(isFullscreen || isCinema) && !isAmbiente" class="exit-btn" @click="isFullscreen ? toggleFullscreen() : toggleCinema()" title="Schließen (ESC)">
|
||
<i data-lucide="x"></i> Schließen
|
||
</button>
|
||
|
||
<!-- Ambiente Exit Button -->
|
||
<button v-if="isAmbiente" class="exit-btn ambiente-exit-btn" @click="isAmbiente = false" title="Ambiente verlassen">
|
||
<Sparkles class="w-4 h-4" />
|
||
</button>
|
||
|
||
<!-- Game Header -->
|
||
<div class="gc-head" v-show="!isFullscreen && !isAmbiente">
|
||
<div class="gc-left">
|
||
<div class="game-icon">
|
||
<img :src="gameMeta?.image || 'https://api.dicebear.com/7.x/shapes/svg?seed=' + titleText" alt="icon" />
|
||
</div>
|
||
<div class="gc-title-area">
|
||
<h1 class="gc-title">{{ titleText }}</h1>
|
||
<div class="gc-subtitle">
|
||
<span class="provider-badge">{{ providerText }}</span>
|
||
<div class="gc-presence" title="Spieler aktiv">
|
||
<span class="presence-dot"></span>
|
||
{{ playerCount }} spielen
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Game Body (Iframe) -->
|
||
<div class="gc-body">
|
||
<!-- Overlays -->
|
||
<div v-if="!iframeSrc && !originalLoading && !loading" class="gp-error">
|
||
<Info class="w-12 h-12 mb-3 text-gray-500" />
|
||
<h2>Spiel nicht verfügbar</h2>
|
||
<p>Die Quelle für dieses Spiel konnte nicht gefunden werden.</p>
|
||
</div>
|
||
|
||
<div v-if="loading" class="gp-loading">
|
||
<div class="loader-ring"></div>
|
||
<div class="loader-logo">
|
||
<PlayCircle class="w-8 h-8 text-[var(--primary)]" />
|
||
</div>
|
||
<span class="loader-text">Lade {{ titleText }}...</span>
|
||
</div>
|
||
|
||
<div v-if="error" class="gp-error">
|
||
<ShieldCheck class="w-12 h-12 mb-3 text-red-500" />
|
||
<h2>Verbindungsfehler</h2>
|
||
<p>{{ error }}</p>
|
||
<Button class="mt-6 neon-button px-8" @click="reload">Neu verbinden</Button>
|
||
</div>
|
||
|
||
<iframe
|
||
v-if="iframeSrc"
|
||
id="game-frame"
|
||
:src="iframeSrc"
|
||
class="gp-iframe"
|
||
allow="autoplay; fullscreen; clipboard-read; clipboard-write"
|
||
allowfullscreen
|
||
@load="onLoad"
|
||
@error="onError"
|
||
></iframe>
|
||
|
||
<!-- Floating tools in native fullscreen -->
|
||
<div v-if="isFullscreen" class="fs-tools">
|
||
<button class="tool-btn bg-black/50 hover:bg-black/80 text-white border-0" @click="toggleFullscreen" title="Vollbild beenden (ESC)">
|
||
<Maximize class="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Stats Overlay -->
|
||
<transition name="slide-fade">
|
||
<aside v-if="showStats" class="gc-stats">
|
||
<div class="stats-head">
|
||
<div class="stats-title"><BarChart2 class="w-4 h-4 mr-2" /> Live Session</div>
|
||
<button class="stats-close" @click="showStats = false">×</button>
|
||
</div>
|
||
<div class="stats-body">
|
||
<div class="stat-box">
|
||
<span class="s-label">Gespielte Runden</span>
|
||
<span class="s-val">0</span>
|
||
</div>
|
||
<div class="stat-box">
|
||
<span class="s-label">Höchster Gewinn</span>
|
||
<span class="s-val text-[var(--primary)]">0.00</span>
|
||
</div>
|
||
<div class="divider-h"></div>
|
||
<div class="stat-row"><span>RTP</span><b>{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'N/A' }}</b></div>
|
||
<div class="stat-row"><span>Volatilität</span><b class="capitalize" :class="'vol-'+gameMeta?.volatility">{{ gameMeta?.volatility || 'N/A' }}</b></div>
|
||
</div>
|
||
</aside>
|
||
</transition>
|
||
</div>
|
||
|
||
<!-- Info Modal Overlay -->
|
||
<transition name="info-modal">
|
||
<div v-if="showInfo" class="info-modal-backdrop" @click.self="showInfo = false">
|
||
<div class="info-modal">
|
||
<div class="info-modal-head">
|
||
<div class="info-modal-title"><Info class="w-5 h-5 mr-2" /> Spielinfo</div>
|
||
<button class="info-modal-close" @click="showInfo = false">×</button>
|
||
</div>
|
||
<div class="info-modal-body">
|
||
<div class="info-game-header">
|
||
<img :src="gameMeta?.image || 'https://api.dicebear.com/7.x/shapes/svg?seed=' + titleText" class="info-thumb" />
|
||
<div>
|
||
<div class="info-game-name">{{ titleText }}</div>
|
||
<div class="info-game-prov">{{ providerText }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-stats-grid">
|
||
<div class="info-stat">
|
||
<span class="is-label">RTP</span>
|
||
<span class="is-val primary">{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-stat">
|
||
<span class="is-label">Volatilität</span>
|
||
<span class="is-val capitalize" :class="'vol-' + gameMeta?.volatility">{{ gameMeta?.volatility || 'N/A' }}</span>
|
||
</div>
|
||
<div class="info-stat">
|
||
<span class="is-label">Kategorie</span>
|
||
<span class="is-val capitalize">{{ gameMeta?.category || 'Slot' }}</span>
|
||
</div>
|
||
<div class="info-stat">
|
||
<span class="is-label">Einsatzlimits</span>
|
||
<span class="is-val">0.10 – 500.00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<h4>Über das Spiel</h4>
|
||
<p>{{ descriptionText }}</p>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<h4>Regeln & Features</h4>
|
||
<ul class="info-rules">
|
||
<li>Linien-Gewinne werden von links nach rechts gezählt</li>
|
||
<li>Höchstgewinn kann je nach Spiel variieren</li>
|
||
<li>Freispiele und Bonusrunden durch Scatter-Symbole</li>
|
||
<li>Wild-Symbole ersetzen alle anderen Symbole außer Scatter</li>
|
||
<li>Alle Gewinne werden als Vielfaches des Einsatzes ausgedrückt</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<h4>Verantwortungsvolles Spielen</h4>
|
||
<p class="info-rg-text">Dieses Spiel sollte nur zur Unterhaltung gespielt werden. Setze dir Limits und spiele verantwortungsbewusst. Bei Fragen wende dich an unseren <a href="/self-exclusion" class="info-link">Verantwortungsvollen Spielerschutz</a>.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- Game Footer Toolbar -->
|
||
<div class="gc-foot" v-show="!isFullscreen && !isAmbiente">
|
||
<div class="foot-left">
|
||
<button class="tool-btn" :class="{ 'text-[var(--primary)]': isFavorite }" @click="toggleFavorite" title="Favorit">
|
||
<Heart class="w-5 h-5" :fill="isFavorite ? 'currentColor' : 'none'" />
|
||
</button>
|
||
<button class="tool-btn" :class="{ active: showStats }" @click="toggleStats" title="Statistik">
|
||
<BarChart2 class="w-5 h-5" />
|
||
</button>
|
||
<button class="tool-btn" :class="{ active: showInfo }" @click="showInfo = !showInfo" title="Spielinfo (RTP, Regeln)">
|
||
<Info class="w-5 h-5" />
|
||
</button>
|
||
<button class="tool-btn" @click="reload" title="Neu laden">
|
||
<RefreshCw class="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div class="foot-center">
|
||
<div class="foot-controls">
|
||
<!-- Demo / Real Switch -->
|
||
<div class="mode-switch" role="group" aria-label="Modus wählen">
|
||
<button class="mode-btn" :class="{ active: mode === 'demo' }" @click="setMode('demo')">Demo</button>
|
||
<button class="mode-btn" :class="{ active: mode === 'real' }" @click="setMode('real')">Real</button>
|
||
</div>
|
||
|
||
<div class="foot-divider" />
|
||
|
||
<!-- Currency Selector -->
|
||
<div class="currency-wrap" ref="currencyMenuRef">
|
||
<button
|
||
class="currency-btn"
|
||
@click="showCurrencyMenu = !showCurrencyMenu"
|
||
title="Währung wählen"
|
||
>
|
||
<span class="currency-dot" :style="{ background: CURRENCIES.find(c => c.symbol === selectedCurrency)?.color }" />
|
||
<span class="currency-label">{{ selectedCurrency }}</span>
|
||
<svg class="currency-chevron" :class="{ open: showCurrencyMenu }" viewBox="0 0 10 6" width="10" height="6"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>
|
||
</button>
|
||
<transition name="cinema-menu">
|
||
<div v-if="showCurrencyMenu" class="currency-dropdown">
|
||
<button
|
||
v-for="c in CURRENCIES"
|
||
:key="c.symbol"
|
||
class="currency-option"
|
||
:class="{ active: selectedCurrency === c.symbol }"
|
||
@click="selectedCurrency = c.symbol; showCurrencyMenu = false"
|
||
>
|
||
<span class="currency-dot" :style="{ background: c.color }" />
|
||
<span class="co-title">{{ c.symbol }}</span>
|
||
<span class="co-desc" style="margin-left: auto">{{ c.name }}</span>
|
||
</button>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="foot-right">
|
||
<button class="tool-btn mr-2" @click="scrollToCenter" title="Spiel zentrieren">
|
||
<Crosshair class="w-5 h-5" />
|
||
</button>
|
||
<div class="cinema-btn-wrap" ref="cinemaMenuRef">
|
||
<button
|
||
class="tool-btn mr-2"
|
||
:class="{ active: isCinema || isAmbiente }"
|
||
@click="showCinemaMenu = !showCinemaMenu"
|
||
title="Ansichts-Modi"
|
||
>
|
||
<MonitorPlay class="w-5 h-5" />
|
||
</button>
|
||
<transition name="cinema-menu">
|
||
<div v-if="showCinemaMenu" class="cinema-dropdown">
|
||
<button
|
||
class="cinema-option"
|
||
:class="{ active: isCinema && !isAmbiente }"
|
||
@click="selectCinema"
|
||
>
|
||
<MonitorPlay class="w-4 h-4 shrink-0" />
|
||
<div>
|
||
<div class="co-title">Kino</div>
|
||
<div class="co-desc">Vergrößerte Ansicht</div>
|
||
</div>
|
||
</button>
|
||
<button
|
||
class="cinema-option"
|
||
:class="{ active: isAmbiente }"
|
||
@click="selectAmbiente"
|
||
>
|
||
<Sparkles class="w-4 h-4 shrink-0" />
|
||
<div>
|
||
<div class="co-title">Ambiente</div>
|
||
<div class="co-desc">Immersives Erlebnis</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
<button class="tool-btn" @click="toggleFullscreen" title="Vollbild (F)">
|
||
<Maximize class="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Content Below Slot -->
|
||
<div class="below-content" v-show="!isCinema && !isFullscreen">
|
||
|
||
<!-- Game Details -->
|
||
<div class="game-details">
|
||
<div class="detail-card">
|
||
<h2 class="section-title">Über {{ titleText }}</h2>
|
||
<p class="desc-text">{{ descriptionText }}</p>
|
||
|
||
<div class="feature-grid">
|
||
<div class="f-item">
|
||
<div class="f-icon"><ShieldCheck class="w-5 h-5 text-[var(--primary)]"/></div>
|
||
<div class="f-info">
|
||
<span>Provider</span>
|
||
<strong>{{ providerText }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="f-item">
|
||
<div class="f-icon"><BarChart2 class="w-5 h-5 text-[var(--primary)]"/></div>
|
||
<div class="f-info">
|
||
<span>Theoretischer RTP</span>
|
||
<strong>{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'Unbekannt' }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="f-item">
|
||
<div class="f-icon"><RefreshCw class="w-5 h-5 text-[var(--primary)]"/></div>
|
||
<div class="f-info">
|
||
<span>Volatilität</span>
|
||
<strong class="capitalize" :class="'vol-'+gameMeta?.volatility">{{ gameMeta?.volatility || 'Unbekannt' }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Live Wins Section (Dashboard Style) -->
|
||
<section class="wins-section">
|
||
<div class="section-header">
|
||
<h2><Trophy class="w-5 h-5 text-yellow-500 inline mb-1" /> Live Wins in {{ titleText }}</h2>
|
||
<div class="flex items-center gap-2 text-xs text-[var(--primary)] font-bold uppercase">
|
||
<span class="w-2 h-2 bg-[var(--primary)] rounded-full animate-pulse"></span> 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">won in {{ win.game }}</div>
|
||
</div>
|
||
<div class="win-value">{{ win.amount }}</div>
|
||
</div>
|
||
</transition-group>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Similar Games Section (Dashboard Style) -->
|
||
<section class="game-section" v-if="similar.length">
|
||
<div class="section-header">
|
||
<h2><Star class="w-5 h-5 text-yellow-400 inline mb-1" /> Mehr von {{ providerText }}</h2>
|
||
</div>
|
||
<div class="game-grid">
|
||
<div v-for="slot in similar" :key="slot.slug" class="similar-game-card group" @click="playGameBySlug(slot.slug, slot.provider)">
|
||
<div class="card-image">
|
||
<img :src="slot.image" :alt="slot.name" loading="lazy">
|
||
<div class="card-overlay">
|
||
<Button class="play-btn" @click.stop.prevent="playGameBySlug(slot.slug, slot.provider)"><PlayCircle class="w-8 h-8 text-white" /></Button>
|
||
<span class="provider">{{ slot.provider }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-info">
|
||
<div class="game-name">{{ slot.name }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
</div>
|
||
</UserLayout>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.gameplay-page {
|
||
padding: 30px;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 30px;
|
||
transition: all 0.5s cubic-bezier(0.2, 0, 0, 1);
|
||
}
|
||
|
||
.gameplay-page.cinema-active {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.gameplay-page.fullscreen-active {
|
||
max-width: 100%;
|
||
padding: 0;
|
||
gap: 0;
|
||
}
|
||
|
||
/* TOP NAV */
|
||
.top-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.back-link {
|
||
display: flex; align-items: center;
|
||
color: #a1a1aa; font-size: 14px; font-weight: 600;
|
||
background: transparent; border: none; cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
.back-link:hover { color: #fff; }
|
||
.game-path {
|
||
font-size: 13px; color: #71717a; font-weight: 500;
|
||
}
|
||
.game-path .sep { margin: 0 8px; opacity: 0.5; }
|
||
.game-path .current { color: #e4e4e7; }
|
||
|
||
|
||
/* MAIN GAME CARD - Full width now */
|
||
.game-card {
|
||
background: #0a0a0a;
|
||
border: 1px solid #151515;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||
transition: all 0.6s cubic-bezier(0.2, 0, 0, 1);
|
||
width: 100%;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Cinema Mode Animation - Make it HUGE */
|
||
.game-card.cinema {
|
||
border-radius: 16px;
|
||
border-color: #222;
|
||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
/* Native Fullscreen Animation */
|
||
.game-card.fullscreen {
|
||
border-radius: 0;
|
||
border: none;
|
||
background: #000;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
}
|
||
|
||
/* Exit Button (for Cinema/Fullscreen) */
|
||
.exit-btn {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
background: rgba(0,0,0,0.6);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
color: #fff;
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
backdrop-filter: blur(10px);
|
||
transition: all 0.2s;
|
||
}
|
||
.exit-btn:hover {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* Header */
|
||
.gc-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 24px;
|
||
background: linear-gradient(to bottom, #111, #0a0a0a);
|
||
border-bottom: 1px solid #151515;
|
||
}
|
||
|
||
.gc-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.game-icon {
|
||
width: 48px; height: 48px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: #111;
|
||
border: 1px solid #222;
|
||
}
|
||
.game-icon img { width: 100%; height: 100%; object-fit: cover; }
|
||
|
||
.gc-title-area {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.gc-title {
|
||
color: #fff;
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
margin: 0; letter-spacing: -0.5px;
|
||
}
|
||
|
||
.gc-subtitle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.provider-badge {
|
||
color: #888;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
background: rgba(255,255,255,0.05);
|
||
padding: 2px 8px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.gc-presence {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.presence-dot {
|
||
width: 6px; height: 6px;
|
||
background: var(--primary);
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 8px var(--primary);
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
/* Mode Switch */
|
||
.mode-switch {
|
||
display: inline-flex;
|
||
background: #050505;
|
||
border: 1px solid #151515;
|
||
border-radius: 10px;
|
||
padding: 4px;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #888;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.mode-btn:hover:not(.active) {
|
||
color: #ccc;
|
||
}
|
||
|
||
.mode-btn.active {
|
||
color: #fff;
|
||
background: #151515;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
/* Body */
|
||
.gc-body {
|
||
position: relative;
|
||
background: radial-gradient(circle at center, #111 0%, #000 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 1;
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
.gp-iframe {
|
||
width: 100%;
|
||
aspect-ratio: 16/9;
|
||
min-height: 600px;
|
||
max-height: 80vh;
|
||
border: none;
|
||
background: transparent;
|
||
display: block;
|
||
transition: all 0.5s ease;
|
||
}
|
||
|
||
/* In cinema mode, make iframe take maximum possible space */
|
||
.game-card.cinema .gp-iframe {
|
||
height: 80vh;
|
||
max-height: 85vh;
|
||
aspect-ratio: auto;
|
||
}
|
||
|
||
.game-card.fullscreen .gp-iframe {
|
||
height: 100vh;
|
||
max-height: none;
|
||
aspect-ratio: auto; /* fill the screen */
|
||
}
|
||
|
||
.fs-tools {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 100;
|
||
}
|
||
|
||
/* Loaders & Errors */
|
||
.gp-loading, .gp-error {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(10, 10, 10, 0.9);
|
||
backdrop-filter: blur(10px);
|
||
z-index: 10;
|
||
text-align: center;
|
||
}
|
||
|
||
.loader-ring {
|
||
width: 80px; height: 80px;
|
||
border: 4px solid rgba(223, 0, 106, 0.1);
|
||
border-left-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
position: absolute;
|
||
}
|
||
|
||
.loader-logo {
|
||
z-index: 2;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.loader-text {
|
||
margin-top: 60px;
|
||
color: #fff; font-size: 15px; font-weight: 600; letter-spacing: 1px;
|
||
}
|
||
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
.gp-error h2 { color: #fff; font-size: 20px; font-weight: 800; margin: 0 0 8px; }
|
||
.gp-error p { color: #888; font-size: 14px; max-width: 300px; }
|
||
|
||
/* Stats Overlay */
|
||
.gc-stats {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: 280px;
|
||
background: rgba(10, 10, 10, 0.95);
|
||
border: 1px solid #222;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
z-index: 20;
|
||
backdrop-filter: blur(16px);
|
||
box-shadow: 0 15px 40px rgba(0,0,0,0.6);
|
||
}
|
||
|
||
.stats-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px;
|
||
background: #151515;
|
||
border-bottom: 1px solid #222;
|
||
}
|
||
|
||
.stats-title { font-size: 14px; font-weight: 700; color: #fff; display: flex; align-items: center; }
|
||
.stats-close { background: transparent; color: #888; border: none; font-size: 20px; cursor: pointer; padding: 0; line-height: 1;}
|
||
.stats-close:hover { color: #fff; }
|
||
|
||
.stats-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
||
|
||
.stat-box {
|
||
background: #0a0a0a; padding: 12px; border-radius: 10px; border: 1px solid #151515;
|
||
display: flex; flex-direction: column; gap: 4px;
|
||
}
|
||
.s-label { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 700; }
|
||
.s-val { font-size: 18px; color: #fff; font-weight: 800; font-family: monospace; }
|
||
|
||
.divider-h { height: 1px; background: #222; margin: 4px 0; }
|
||
|
||
.stat-row { display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
|
||
.stat-row span { color: #888; }
|
||
.stat-row b { color: #fff; font-weight: 600; }
|
||
.vol-high { color: #ff007a; }
|
||
.vol-medium { color: #ffb700; }
|
||
.vol-low { color: #00ff9d; }
|
||
|
||
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.3s ease; }
|
||
.slide-fade-enter-from, .slide-fade-leave-to { opacity: 0; transform: translateX(-20px); }
|
||
|
||
/* Footer Toolbar */
|
||
.gc-foot {
|
||
padding: 16px 24px;
|
||
background: #0a0a0a;
|
||
border-top: 1px solid #151515;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.foot-left, .foot-right { display: flex; align-items: center; gap: 8px; flex: 1; }
|
||
.foot-right { justify-content: flex-end; }
|
||
|
||
.tool-btn {
|
||
width: 40px; height: 40px;
|
||
border-radius: 10px;
|
||
background: #111;
|
||
border: 1px solid #222;
|
||
color: #888;
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.tool-btn:hover { color: #fff; background: #151515; transform: translateY(-2px); }
|
||
.tool-btn.active { color: var(--primary); border-color: var(--primary); background: rgba(223, 0, 106, 0.1); }
|
||
|
||
/* CONTENT BELOW SLOT */
|
||
.below-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 50px;
|
||
padding: 0 0 40px;
|
||
}
|
||
|
||
/* EXTRA INFO */
|
||
.game-details { width: 100%; }
|
||
.detail-card {
|
||
background: #0a0a0a; border: 1px solid #151515; border-radius: 20px; padding: 30px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||
}
|
||
.section-title { color: #fff; font-size: 18px; font-weight: 800; margin-bottom: 16px; letter-spacing: 1px; text-transform: uppercase;}
|
||
.desc-text { color: #888; font-size: 14px; line-height: 1.7; margin-bottom: 30px; max-width: 1000px;}
|
||
|
||
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
|
||
.f-item { display: flex; align-items: center; gap: 16px; background: #111; padding: 16px; border-radius: 12px; border: 1px solid #222;}
|
||
.f-icon { width: 40px; height: 40px; background: #151515; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
|
||
.f-info { display: flex; flex-direction: column; }
|
||
.f-info span { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 700; }
|
||
.f-info strong { font-size: 14px; color: #fff; font-weight: 600; }
|
||
|
||
/* Dashboard Style Sections */
|
||
.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;}
|
||
|
||
/* Wins Section */
|
||
.wins-section { width: 100%; }
|
||
.wins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 15px; }
|
||
.win-card { background: #0a0a0a; border: 1px solid #151515; border-radius: 12px; padding: 12px 15px; display: flex; align-items: center; gap: 12px; transition: all 0.3s; }
|
||
.win-card:hover { border-color: #333; background: #111; transform: translateY(-2px); }
|
||
.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; }
|
||
.wins-list-move, .wins-list-enter-active, .wins-list-leave-active { transition: all 0.5s ease; position: relative; }
|
||
.wins-list-enter-from { opacity: 0; transform: translateY(-20px); }
|
||
.wins-list-leave-to { opacity: 0; transform: scale(0.9); }
|
||
|
||
/* Game Grid */
|
||
.game-section { width: 100%; }
|
||
.game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
||
.similar-game-card { background: #0a0a0a; border-radius: 16px; overflow: hidden; border: 1px solid #151515; transition: transform 0.3s, box-shadow 0.3s; cursor: pointer; position: relative; }
|
||
.similar-game-card:hover { transform: translateY(-8px); box-shadow: 0 15px 40px rgba(0,0,0,0.6); border-color: #333; 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; }
|
||
.similar-game-card:hover img { transform: scale(1.1); filter: brightness(0.4); }
|
||
.card-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; }
|
||
.similar-game-card:hover .card-overlay { opacity: 1; }
|
||
.play-btn { width: 60px; height: 60px; border-radius: 50%; background: transparent; color: white; display: flex; align-items: center; justify-content: center; margin-bottom: 10px; transform: scale(0.8); transition: transform 0.2s; border: none; padding: 0;}
|
||
.similar-game-card:hover .play-btn { transform: scale(1); }
|
||
.play-btn:hover { filter: brightness(1.2); drop-shadow: 0 0 10px rgba(255,255,255,0.5);}
|
||
.provider { font-size: 10px; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 1px; }
|
||
.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; }
|
||
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1024px) {
|
||
.gc-foot { flex-wrap: wrap; gap: 16px; }
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.gameplay-page { padding: 12px; }
|
||
.gc-head { flex-direction: column; align-items: flex-start; gap: 16px; }
|
||
.gc-right { width: 100%; justify-content: space-between; }
|
||
.gp-iframe { aspect-ratio: auto; height: 50vh; }
|
||
.foot-left, .foot-right { justify-content: center; width: 100%; }
|
||
}
|
||
|
||
/* Info Modal */
|
||
.info-modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 8000;
|
||
background: rgba(0, 0, 0, 0.75);
|
||
backdrop-filter: blur(6px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16px;
|
||
}
|
||
|
||
.info-modal {
|
||
background: #0d0d0d;
|
||
border: 1px solid #222;
|
||
border-radius: 18px;
|
||
width: 100%;
|
||
max-width: 520px;
|
||
max-height: 88vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.8), 0 0 40px rgba(223, 0, 106, 0.08);
|
||
}
|
||
|
||
.info-modal-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px 16px;
|
||
border-bottom: 1px solid #1a1a1a;
|
||
position: sticky;
|
||
top: 0;
|
||
background: #0d0d0d;
|
||
z-index: 1;
|
||
}
|
||
|
||
.info-modal-title {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
}
|
||
|
||
.info-modal-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: #1a1a1a;
|
||
border: 1px solid #2a2a2a;
|
||
color: #888;
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s;
|
||
}
|
||
.info-modal-close:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||
|
||
.info-modal-body { padding: 20px 24px 24px; display: flex; flex-direction: column; gap: 20px; }
|
||
|
||
.info-game-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.info-thumb {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 12px;
|
||
object-fit: cover;
|
||
border: 1px solid #222;
|
||
}
|
||
|
||
.info-game-name { font-size: 17px; font-weight: 800; color: #fff; }
|
||
.info-game-prov { font-size: 12px; color: #888; margin-top: 3px; }
|
||
|
||
.info-stats-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.info-stat {
|
||
background: #111;
|
||
border: 1px solid #1a1a1a;
|
||
border-radius: 10px;
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.is-label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
.is-val { font-size: 15px; font-weight: 800; color: #fff; }
|
||
.is-val.primary { color: var(--primary); }
|
||
|
||
.info-section h4 {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
color: #aaa;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
.info-section p {
|
||
font-size: 13px;
|
||
color: #888;
|
||
line-height: 1.6;
|
||
margin: 0;
|
||
}
|
||
|
||
.info-rules {
|
||
margin: 0;
|
||
padding: 0 0 0 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.info-rules li { font-size: 12px; color: #777; line-height: 1.5; }
|
||
|
||
.info-rg-text { font-size: 12px; color: #555; }
|
||
.info-link { color: var(--primary); text-decoration: none; }
|
||
.info-link:hover { text-decoration: underline; }
|
||
|
||
/* ── FOOT-CENTER CONTROLS ──────────────────────────────────────────────── */
|
||
.foot-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.foot-divider {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: #222;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Currency selector */
|
||
.currency-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.currency-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
height: 40px;
|
||
padding: 0 12px;
|
||
border-radius: 10px;
|
||
background: #111;
|
||
border: 1px solid #222;
|
||
color: #ccc;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
.currency-btn:hover {
|
||
background: #151515;
|
||
color: #fff;
|
||
border-color: #333;
|
||
}
|
||
|
||
.currency-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.currency-label {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.currency-chevron {
|
||
color: #666;
|
||
transition: transform 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
.currency-chevron.open {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.currency-dropdown {
|
||
position: absolute;
|
||
bottom: calc(100% + 10px);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: #0d0d0d;
|
||
border: 1px solid #222;
|
||
border-radius: 14px;
|
||
padding: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
min-width: 200px;
|
||
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
|
||
z-index: 200;
|
||
}
|
||
|
||
.currency-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 9px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid transparent;
|
||
background: transparent;
|
||
color: #888;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: all 0.15s;
|
||
width: 100%;
|
||
}
|
||
.currency-option:hover {
|
||
background: #151515;
|
||
color: #fff;
|
||
border-color: #2a2a2a;
|
||
}
|
||
.currency-option.active {
|
||
background: rgba(223, 0, 106, 0.1);
|
||
border-color: rgba(223, 0, 106, 0.3);
|
||
color: var(--primary);
|
||
}
|
||
.currency-option.active .co-desc {
|
||
color: rgba(223, 0, 106, 0.5);
|
||
}
|
||
|
||
/* Info modal transition */
|
||
.info-modal-enter-active, .info-modal-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
|
||
.info-modal-enter-from, .info-modal-leave-to { opacity: 0; transform: scale(0.95); }
|
||
|
||
/* ── AMBIENTE MODE ─────────────────────────────────────────────────────── */
|
||
|
||
/* Fixed ambient glow that fills the entire viewport background */
|
||
.ambient-bg {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
background: #000;
|
||
}
|
||
.ambient-bg::before {
|
||
content: '';
|
||
position: absolute;
|
||
width: 140vmax;
|
||
height: 140vmax;
|
||
top: 50%;
|
||
left: 50%;
|
||
background: conic-gradient(
|
||
from 0deg at 50% 50%,
|
||
rgba(223, 0, 106, 0.08),
|
||
rgba(124, 58, 237, 0.06),
|
||
rgba(14, 165, 233, 0.05),
|
||
rgba(16, 185, 129, 0.04),
|
||
rgba(245, 158, 11, 0.05),
|
||
rgba(223, 0, 106, 0.08)
|
||
);
|
||
filter: blur(120px);
|
||
animation: ambient-spin 30s linear infinite;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
.ambient-bg::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 80vmax;
|
||
height: 80vmax;
|
||
top: 50%;
|
||
left: 50%;
|
||
background: conic-gradient(
|
||
from 180deg at 50% 50%,
|
||
rgba(124, 58, 237, 0.05),
|
||
rgba(223, 0, 106, 0.06),
|
||
rgba(14, 165, 233, 0.04),
|
||
rgba(124, 58, 237, 0.05)
|
||
);
|
||
filter: blur(100px);
|
||
animation: ambient-spin 22s linear infinite reverse;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
@keyframes ambient-spin {
|
||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||
}
|
||
|
||
/* The gameplay-page sits above the ambient background */
|
||
.gameplay-page.ambiente-active {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* Card in ambiente: no head/foot borders, glass feel, animated edge glow */
|
||
.game-card.ambiente {
|
||
background: rgba(5, 5, 5, 0.85);
|
||
border-color: rgba(255, 255, 255, 0.07);
|
||
backdrop-filter: blur(24px);
|
||
animation: card-ambient-glow 5s ease-in-out infinite alternate;
|
||
}
|
||
@keyframes card-ambient-glow {
|
||
from { box-shadow: 0 0 40px rgba(0,0,0,0.9), 0 0 30px rgba(223, 0, 106, 0.12); }
|
||
to { box-shadow: 0 0 70px rgba(0,0,0,0.95), 0 0 60px rgba(223, 0, 106, 0.28), 0 0 100px rgba(124, 58, 237, 0.14); }
|
||
}
|
||
|
||
/* Ambiente exit button – top-right, distinct from cinema exit */
|
||
.ambiente-exit-btn {
|
||
background: rgba(223, 0, 106, 0.12);
|
||
border-color: rgba(223, 0, 106, 0.35);
|
||
color: var(--primary);
|
||
padding: 8px;
|
||
gap: 0;
|
||
}
|
||
.ambiente-exit-btn:hover {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
/* ── CINEMA DROPDOWN ───────────────────────────────────────────────────── */
|
||
.cinema-btn-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.cinema-dropdown {
|
||
position: absolute;
|
||
bottom: calc(100% + 10px);
|
||
right: 0;
|
||
background: #0d0d0d;
|
||
border: 1px solid #222;
|
||
border-radius: 14px;
|
||
padding: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 190px;
|
||
box-shadow: 0 20px 50px rgba(0,0,0,0.8), 0 0 30px rgba(0,0,0,0.5);
|
||
z-index: 200;
|
||
}
|
||
|
||
.cinema-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid transparent;
|
||
background: transparent;
|
||
color: #888;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: all 0.2s;
|
||
width: 100%;
|
||
}
|
||
.cinema-option:hover {
|
||
background: #151515;
|
||
color: #fff;
|
||
border-color: #2a2a2a;
|
||
}
|
||
.cinema-option.active {
|
||
background: rgba(223, 0, 106, 0.1);
|
||
border-color: rgba(223, 0, 106, 0.3);
|
||
color: var(--primary);
|
||
}
|
||
.co-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
|
||
.co-desc { font-size: 11px; color: #555; margin-top: 2px; }
|
||
.cinema-option.active .co-desc { color: rgba(223, 0, 106, 0.6); }
|
||
|
||
|
||
.cinema-menu-enter-active, .cinema-menu-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
|
||
.cinema-menu-enter-from, .cinema-menu-leave-to { opacity: 0; transform: translateY(6px); }
|
||
</style>
|