Feat: footer sémantique dans Blog, About et Article
All checks were successful
Deploy / Deploy to Production (push) Successful in 19s

Refonte complète du Footer (sémantique propre, données dynamiques depuis
le store site, bouton subscribe avec l'animation .button partagée).
Intégré dans About, Blog et Article. Scroll-to-top au départ de About et Blog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-11 10:57:52 +01:00
parent eef95f10d6
commit 0233a6a4a4
4 changed files with 335 additions and 44 deletions

View file

@ -1,73 +1,341 @@
<script> <script>
import { site } from '@state/site.svelte' import { site } from '@state/site.svelte'
const siteTitle = $derived(site.title || 'World Game') const logo = $derived(site.logo)
const currentYear = $derived(new Date().getFullYear()) const title = $derived(site.title || 'World Game')
const contact = $derived(site.contact || {})
const socials = $derived(contact.socials ?? [])
const year = new Date().getFullYear()
let email = $state('')
let status = $state(null)
async function handleSubscribe(e) {
e.preventDefault()
if (!email) return
try {
const res = await fetch('/newsletter.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
})
if (res.ok) {
status = { type: 'success', message: 'Merci pour votre inscription !' }
email = ''
} else {
status = { type: 'error', message: 'Une erreur est survenue.' }
}
} catch {
status = { type: 'error', message: 'Une erreur est survenue.' }
}
}
</script> </script>
<footer class="footer"> <footer class="site-footer">
<div class="footer__container"> <div class="footer-main">
<div class="footer__brand">
<p class="footer__title">{siteTitle}</p> <!-- Logo -->
<p class="footer__tagline">Play to Engage</p> {#if logo}
<div class="footer-brand">
<img src={logo} alt={title} />
</div>
{/if}
<!-- Location -->
{#if contact.address}
<div class="footer-col">
<h3>Location</h3>
<address>{@html contact.address}</address>
</div>
{/if}
<!-- Contact -->
{#if contact.email}
<div class="footer-col">
<h3>Contact</h3>
<a href="mailto:{contact.email}">{contact.email}</a>
</div>
{/if}
<!-- Follow us -->
{#if socials.length > 0}
<div class="footer-col">
<h3>Follow us</h3>
<ul class="footer-socials" role="list">
{#each socials as social}
<li>
<a href={social.url} target="_blank" rel="noopener noreferrer" aria-label={social.label}>
{#if social.picto}
{@html social.picto}
{:else}
{social.label}
{/if}
</a>
</li>
{/each}
</ul>
</div>
{/if}
<!-- Newsletter -->
<div class="footer-newsletter">
<h3>Subscribe to our newsletters!</h3>
<form class="footer-newsletter-form" onsubmit={handleSubscribe}>
<input
class="footer-newsletter-input"
type="email"
placeholder="Enter your email"
bind:value={email}
required
/>
<button type="submit" class="button">Subscribe</button>
</form>
{#if status}
<p class="footer-newsletter-status footer-newsletter-status--{status.type}">
{status.message}
</p>
{/if}
</div> </div>
<div class="footer__copyright"> </div>
<p>&copy; {currentYear} {siteTitle}. Tous droits réservés.</p>
</div> <!-- Copyright bar -->
<div class="footer-bottom">
<p>World Game &copy; {year}. All rights reserved.</p>
<div class="footer-divider" aria-hidden="true"></div>
<a href="/cookies">Cookies preferences</a>
<div class="footer-divider" aria-hidden="true"></div>
{#if contact.legalNotice}
<a href={contact.legalNotice} target="_blank" rel="noopener noreferrer">Mentions légales</a>
{:else}
<a href="/privacy">Privacy</a>
{/if}
</div> </div>
</footer> </footer>
<style> <style>
.footer { .site-footer {
background: #000; width: 100vw;
color: #fff; background: #0d0e22;
padding: 3rem 2rem 2rem;
margin-top: auto;
} }
.footer__container { /* --- Main row --- */
max-width: 1400px; .footer-main {
margin: 0 auto;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
gap: 2rem; justify-content: space-between;
padding: 24px 45px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
} }
.footer__brand { /* --- Brand --- */
flex: 1; .footer-brand img {
height: 40px;
width: auto;
} }
.footer__title { /* --- Info columns --- */
font-size: 1.5rem; .footer-col {
font-weight: 700; display: flex;
margin: 0 0 0.5rem; flex-direction: column;
gap: 16px;
width: 136px;
} }
.footer__tagline { .footer-col h3 {
color: #04fea0; color: #fff;
margin: 0; font-family: "Danzza", sans-serif;
font-size: 0.9rem; font-size: var(--font-size-paragraph);
font-weight: 500;
text-transform: uppercase;
line-height: 16px;
} }
.footer__copyright { .footer-col address,
font-size: 0.85rem; .footer-col address a,
color: rgba(255, 255, 255, 0.6); .footer-col a {
color: #4dfca1;
font-family: "Danzza", sans-serif;
font-size: var(--font-size-paragraph-small);
font-style: normal;
line-height: 20px;
text-decoration: none;
} }
.footer__copyright p { /* --- Socials --- */
margin: 0; .footer-socials {
display: flex;
gap: 24px;
list-style: none;
} }
@media (max-width: 768px) { .footer-socials a {
.footer__container { width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-socials :global(svg),
.footer-socials img {
width: 24px;
height: 24px;
}
/* --- Newsletter --- */
.footer-newsletter {
width: 493px;
display: flex;
flex-direction: column;
gap: 12px;
}
.footer-newsletter h3 {
color: #fff;
font-family: "Danzza", sans-serif;
font-size: var(--font-size-paragraph);
font-weight: 500;
text-transform: uppercase;
line-height: 16px;
}
.footer-newsletter-form {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.footer-newsletter-input {
width: 315px;
height: 45px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid #fff;
color: #fff;
font-size: 16px;
font-family: "Danzza", sans-serif;
}
.footer-newsletter-input::placeholder {
opacity: 0.7;
color: #fff;
}
/* Override .button width for footer context */
.footer-newsletter-form :global(.button) {
width: auto;
min-width: unset;
position: static;
height: 45px;
}
.footer-newsletter-status {
padding: 8px;
border-radius: 4px;
font-size: 14px;
text-align: center;
}
.footer-newsletter-status--success {
background: #4dfca1;
color: #1e1938;
}
.footer-newsletter-status--error {
background: #ff6b6b;
color: #fff;
}
/* --- Copyright bar --- */
.footer-bottom {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
padding: 24px 45px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.footer-bottom p,
.footer-bottom a {
color: #fff;
font-family: "Danzza", sans-serif;
font-size: 16px;
font-weight: 400;
text-transform: uppercase;
text-decoration: none;
}
.footer-divider {
width: 1px;
height: 24px;
background: #4dfca1;
}
/* --- Mobile (≤700px) --- */
@media (max-width: 700px) {
.footer-main {
flex-direction: column; flex-direction: column;
text-align: center; align-items: flex-start;
gap: 20px;
padding: 16px 20px;
} }
.footer__copyright { .footer-newsletter {
order: 2; width: 100%;
}
.footer-newsletter-input {
width: 100%;
flex-grow: 1;
}
.footer-newsletter-form :global(.button) {
width: 100%;
margin-top: 10px;
}
.footer-bottom {
flex-direction: column;
gap: 16px;
text-align: center;
padding: 16px 20px;
}
.footer-divider {
width: 100%;
height: 1px;
}
}
/* --- Tablet (701px912px) --- */
@media (min-width: 701px) and (max-width: 912px) {
.footer-main {
flex-direction: column;
align-items: flex-start;
gap: 20px;
padding: 20px 30px;
}
.footer-newsletter {
width: 100%;
}
.footer-newsletter-input {
width: 60%;
}
.footer-bottom {
flex-direction: column;
gap: 16px;
text-align: center;
padding: 20px 30px;
}
.footer-divider {
width: 100%;
height: 1px;
} }
} }
</style> </style>

View file

@ -1,15 +1,24 @@
<script> <script>
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import { slides } from '@state/slides.svelte'
import Footer from '@components/layout/Footer.svelte'
let { data } = $props() let { data } = $props()
const intro = $derived(data?.intro || {}) const intro = $derived(data?.intro || {})
const mission = $derived(data?.mission || {}) const mission = $derived(data?.mission || {})
const manifesto = $derived(data?.manifesto || {}) const manifesto = $derived(data?.manifesto || {})
const team = $derived(data?.team || {}) const team = $derived(data?.team || {})
const isActive = $derived(slides.active?.id === 'about')
let sectionEl = $state(null)
$effect(() => {
if (!isActive) sectionEl?.scrollTo(0, 0)
})
</script> </script>
<div class="about" transition:fade> <div class="about page-scrollable" bind:this={sectionEl} transition:fade>
<section class="about__intro"> <section class="about__intro">
<h1>{intro.title || data?.title}</h1> <h1>{intro.title || data?.title}</h1>
{#if intro.text} {#if intro.text}
@ -61,6 +70,8 @@
</div> </div>
</section> </section>
{/if} {/if}
<Footer />
</div> </div>
<style> <style>
@ -165,6 +176,10 @@
opacity: 0.8; opacity: 0.8;
} }
.about :global(.site-footer) {
margin-left: -2rem;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.about { .about {
padding: 6rem 1rem 3rem; padding: 6rem 1rem 3rem;

View file

@ -3,6 +3,8 @@
* Vue article — sous-composant de Blog.svelte. * Vue article — sous-composant de Blog.svelte.
* Reçoit les données article via props, pas via le slide system. * Reçoit les données article via props, pas via le slide system.
*/ */
import Footer from '@components/layout/Footer.svelte'
let { data, onBack } = $props() let { data, onBack } = $props()
let copySuccess = $state(false) let copySuccess = $state(false)
@ -111,6 +113,8 @@
</article> </article>
<Footer />
<style> <style>
.article { .article {
grid-area: 6 / 1 / span 10 / span 20; grid-area: 6 / 1 / span 10 / span 20;

View file

@ -307,6 +307,10 @@
opacity: 0.6; opacity: 0.6;
} }
.blog :global(.site-footer) {
margin-left: -50px;
}
/* --- Mobile --- */ /* --- Mobile --- */
@media (max-width: 700px) { @media (max-width: 700px) {
.blog-header { .blog-header {