Feat: page Jouer renommée Play + vue implémentée
All checks were successful
Deploy / Deploy to Production (push) Successful in 20s
All checks were successful
Deploy / Deploy to Production (push) Successful in 20s
Renommage jouer → play (blueprint, template, txt, vue) API play.json.php : title, lettering, description, thumbnail, background_image, play_links par jeu enfant listed() Vue Play.svelte : - Fond image avec crossfade (transition opacity sur .play-bg) - Section principale (.play-featured) : glissé/fondu au changement de jeu via --slide-dir CSS var + classes is-out - Carrousel nav (ul > li > button) : thumbnail + titre, active bordure verte - Jeu sans play_links → bouton "Coming soon" désactivé - Layout golden-grid, mobile adapté Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3ce350d2d7
commit
6151dd5a31
7 changed files with 319 additions and 59 deletions
294
src/views/Play.svelte
Normal file
294
src/views/Play.svelte
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue