world-game/src/views/Play.svelte

295 lines
6.9 KiB
Svelte
Raw Normal View History

<script>
import { onMount } from 'svelte'
import { slides } from '@state/slides.svelte'
let { data } = $props()
// --- Derived ---
const isActive = $derived(slides.active?.id === 'jouer')
const games = $derived(data?.games ?? [])
// --- State ---
let currentIndex = $state(0) // index de la sélection (mis à jour immédiatement)
let displayedIndex = $state(0) // index affiché (mis à jour au milieu de la transition)
let isOut = $state(false)
let isTransitioning = $state(false)
let slideDir = $state(1) // -1 : vers la droite, 1 : vers la gauche
const displayedGame = $derived(games[displayedIndex] ?? null)
let t1 = null
let t2 = null
function selectGame(i) {
if (i === currentIndex || isTransitioning || !games.length) return
slideDir = i > currentIndex ? -1 : 1
currentIndex = i
isOut = true
isTransitioning = true
clearTimeout(t1)
clearTimeout(t2)
// Milieu : swap du contenu affiché et du fond (éléments invisibles à ce moment)
t1 = setTimeout(() => {
displayedIndex = i
isOut = false
}, 300)
// Fin de transition
t2 = setTimeout(() => {
isTransitioning = false
}, 600)
}
// Réinitialisation quand on quitte la slide
$effect(() => {
if (!isActive) {
clearTimeout(t1)
clearTimeout(t2)
currentIndex = 0
displayedIndex = 0
isOut = false
isTransitioning = false
}
})
onMount(() => () => {
clearTimeout(t1)
clearTimeout(t2)
})
</script>
<section class="play golden-grid slide" aria-label="Jouer">
<!-- Fond : image + overlay, crossfade au changement de jeu -->
<div class="play-bg" class:is-out={isOut} aria-hidden="true">
{#if displayedGame?.background_image}
<img class="play-bg-img" src={displayedGame.background_image} alt="" />
{/if}
<div class="play-bg-overlay"></div>
</div>
<!-- Lignes verticales décoratives -->
<div class="vertical-line-start" aria-hidden="true"></div>
<div class="vertical-line vertical-line-col8" aria-hidden="true"></div>
<div class="vertical-line-center" aria-hidden="true"></div>
<div class="vertical-line vertical-line-col14" aria-hidden="true"></div>
<div class="vertical-line-end" aria-hidden="true"></div>
<!-- Section principale : lettering + description + boutons de jeu -->
<div
class="play-featured"
class:is-out={isOut}
style="--slide-dir: {slideDir}"
aria-live="polite"
>
{#if displayedGame}
{#if displayedGame.lettering}
<img
class="play-lettering"
src={displayedGame.lettering}
alt={displayedGame.title}
/>
{/if}
{#if displayedGame.description}
<div class="play-description">{@html displayedGame.description}</div>
{/if}
<div class="play-actions">
{#if displayedGame.play_links?.length}
{#each displayedGame.play_links as link}
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
class="button"
>{link.label}</a>
{/each}
{:else}
<button class="button" disabled>Coming soon</button>
{/if}
</div>
{/if}
</div>
<!-- Carrousel de sélection des jeux -->
<nav class="play-carousel" aria-label="Choisir un jeu">
<ul role="list">
{#each games as game, i}
<li class="play-carousel-item" class:active={i === currentIndex}>
<button
aria-label={game.title}
aria-current={i === currentIndex ? 'true' : undefined}
onclick={() => selectGame(i)}
>
{#if game.thumbnail}
<img src={game.thumbnail} alt="" />
{/if}
<span class="play-carousel-title">{game.title}</span>
</button>
</li>
{/each}
</ul>
</nav>
</section>
<style>
.play {
background: #000;
}
/* --- Background --- */
.play-bg {
grid-area: 1/1 / span 20 / span 20;
position: relative;
overflow: hidden;
transition: opacity 0.4s ease;
}
.play-bg.is-out {
opacity: 0;
}
.play-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.play-bg-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to right,
rgba(0, 0, 0, 0.75) 0%,
rgba(0, 0, 0, 0.35) 60%,
rgba(0, 0, 0, 0.1) 100%
);
}
/* --- Lignes verticales --- */
.vertical-line-col8 {
grid-area: 1/8 / span 20 / span 1;
}
.vertical-line-col14 {
grid-area: 1/14 / span 20 / span 1;
}
/* --- Section principale --- */
.play-featured {
grid-area: 4/4 / span 12 / span 10;
z-index: var(--z-content);
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.play-featured.is-out {
opacity: 0;
transform: translateX(calc(var(--slide-dir) * 40px));
}
.play-lettering {
max-height: 120px;
width: auto;
object-fit: contain;
object-position: left center;
}
.play-description {
font-size: var(--font-size-paragraph);
color: var(--color-text);
line-height: 1.5;
max-width: 42ch;
}
.play-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* --- Carrousel --- */
.play-carousel {
grid-area: 16/4 / span 3 / span 14;
z-index: var(--z-content);
}
.play-carousel ul {
list-style: none;
display: flex;
gap: 1.5rem;
align-items: flex-end;
height: 100%;
}
.play-carousel-item button {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.4s var(--ease-standard), transform 0.4s var(--ease-standard);
}
.play-carousel-item button img {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 12px;
border: 2px solid transparent;
transition: border-color 0.4s var(--ease-standard);
}
.play-carousel-title {
font-size: var(--font-size-caption);
color: var(--color-text);
text-align: center;
}
.play-carousel-item.active button {
opacity: 1;
transform: scale(1.15) translateY(-4px);
}
.play-carousel-item.active button img {
border-color: var(--color-primary);
}
/* --- Mobile (≤ 700px) --- */
@media screen and (max-width: 700px) {
.play-bg-overlay {
background: rgba(0, 0, 0, 0.6);
}
.play-featured {
grid-area: 3/2 / span 12 / span 18;
}
.play-carousel {
grid-area: 16/2 / span 3 / span 18;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.play-featured,
.play-bg,
.play-carousel-item button {
transition: none;
}
}
</style>