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
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
$specificData = [
|
|
||||||
'intro' => [
|
|
||||||
'title' => $page->intro_title()->value(),
|
|
||||||
'text' => $page->intro_text()->value()
|
|
||||||
],
|
|
||||||
'games' => $page->children()->listed()->map(function($game) {
|
|
||||||
$badgeValue = $game->badge()->value();
|
|
||||||
$badgeLabel = 'none';
|
|
||||||
if ($badgeValue === 'new') {
|
|
||||||
$badgeLabel = 'NEW';
|
|
||||||
} elseif ($badgeValue === 'coming_soon') {
|
|
||||||
$badgeLabel = 'INCOMING';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'title' => $game->title()->value(),
|
|
||||||
'slug' => $game->slug(),
|
|
||||||
'url' => $game->url(),
|
|
||||||
'description' => $game->description()->value(),
|
|
||||||
'cover' => $game->cover()->toFile()?->url(),
|
|
||||||
'badge' => $badgeValue,
|
|
||||||
'badge_label' => $badgeLabel,
|
|
||||||
'game_status' => $game->game_status()->value()
|
|
||||||
];
|
|
||||||
})->values()
|
|
||||||
];
|
|
||||||
|
|
||||||
$pageData = array_merge($genericData, $specificData);
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
echo json_encode($pageData);
|
|
||||||
23
site/templates/play.json.php
Normal file
23
site/templates/play.json.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$specificData = [
|
||||||
|
'games' => $page->children()->listed()->map(function($game) {
|
||||||
|
return [
|
||||||
|
'title' => $game->title()->value(),
|
||||||
|
'slug' => $game->slug(),
|
||||||
|
'lettering' => $game->lettering()->toFile()?->url(),
|
||||||
|
'description' => $game->description()->value(),
|
||||||
|
'thumbnail' => $game->thumbnail()->toFile()?->url(),
|
||||||
|
'background_image' => $game->background_image()->toFile()?->url(),
|
||||||
|
'play_links' => $game->play_links()->toStructure()->map(fn($l) => [
|
||||||
|
'label' => $l->label()->value(),
|
||||||
|
'url' => $l->url()->value(),
|
||||||
|
])->values(),
|
||||||
|
];
|
||||||
|
})->values()
|
||||||
|
];
|
||||||
|
|
||||||
|
$pageData = array_merge($genericData, $specificData);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($pageData);
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
import Expertise from '@views/Expertise.svelte'
|
import Expertise from '@views/Expertise.svelte'
|
||||||
import Portfolio from '@views/Portfolio.svelte'
|
import Portfolio from '@views/Portfolio.svelte'
|
||||||
import Project from '@views/Project.svelte'
|
import Project from '@views/Project.svelte'
|
||||||
import Jouer from '@views/Jouer.svelte'
|
import Play from '@views/Play.svelte'
|
||||||
import Game from '@views/Game.svelte'
|
import Game from '@views/Game.svelte'
|
||||||
import Blog from '@views/Blog.svelte'
|
import Blog from '@views/Blog.svelte'
|
||||||
import Article from '@views/Article.svelte'
|
import Article from '@views/Article.svelte'
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
expertise: Expertise,
|
expertise: Expertise,
|
||||||
portfolio: Portfolio,
|
portfolio: Portfolio,
|
||||||
project: Project,
|
project: Project,
|
||||||
jouer: Jouer,
|
play: Play,
|
||||||
game: Game,
|
game: Game,
|
||||||
blog: Blog,
|
blog: Blog,
|
||||||
article: Article,
|
article: Article,
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<script>
|
|
||||||
import { fade } from 'svelte/transition'
|
|
||||||
let { data } = $props()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="jouer" transition:fade>
|
|
||||||
<div class="jouer__container">
|
|
||||||
<h1>{data?.title || 'Jouer'}</h1>
|
|
||||||
<p>Jouer view - To be implemented</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.jouer {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 8rem 2rem 4rem;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jouer__container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
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