add privacy page as standalone SPA view outside slide navigation
- New Kirby template/blueprint/JSON for privacy page (confidentialite slug) - Standalone page state in slides store + router handling for non-nav pages - Privacy.svelte view with background image, text blocks, footer - Centralize vertical lines in App.svelte as fixed elements with per-slide visibility - Footer privacy link language-aware (FR/EN) - Portfolio mockup fix: read from default language for consistent EN display - menu.php: add privacy page to Kirby panel navigation refs #44 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b12b839f1b
commit
44af8a9b4e
11 changed files with 259 additions and 50 deletions
41
site/blueprints/pages/privacy.yml
Normal file
41
site/blueprints/pages/privacy.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
title: Confidentialité
|
||||||
|
icon: lock
|
||||||
|
|
||||||
|
tabs:
|
||||||
|
content:
|
||||||
|
label: Contenu
|
||||||
|
icon: text
|
||||||
|
columns:
|
||||||
|
- width: 1/3
|
||||||
|
fields:
|
||||||
|
backgroundImage:
|
||||||
|
label: Image de fond
|
||||||
|
type: files
|
||||||
|
max: 1
|
||||||
|
layout: cards
|
||||||
|
accept: image/*
|
||||||
|
translate: false
|
||||||
|
|
||||||
|
- width: 2/3
|
||||||
|
fields:
|
||||||
|
body:
|
||||||
|
label: Corps
|
||||||
|
type: blocks
|
||||||
|
fieldsets:
|
||||||
|
text:
|
||||||
|
extends: blocks/text
|
||||||
|
fields:
|
||||||
|
text:
|
||||||
|
headings:
|
||||||
|
- 3
|
||||||
|
nodes:
|
||||||
|
- heading
|
||||||
|
marks:
|
||||||
|
- bold
|
||||||
|
- italic
|
||||||
|
- underline
|
||||||
|
- strike
|
||||||
|
- link
|
||||||
|
|
||||||
|
files: tabs/files
|
||||||
|
seo: seo/page
|
||||||
|
|
@ -30,7 +30,8 @@ return [
|
||||||
'jouer' => menuItem('jouer', 'Jouer', 'play', 'pages/jouer'),
|
'jouer' => menuItem('jouer', 'Jouer', 'play', 'pages/jouer'),
|
||||||
'a-propos' => menuItem('a-propos', 'À propos', 'users', 'pages/a-propos'),
|
'a-propos' => menuItem('a-propos', 'À propos', 'users', 'pages/a-propos'),
|
||||||
'blog' => menuItem('blog', 'Blog', 'text', 'pages/blog'),
|
'blog' => menuItem('blog', 'Blog', 'text', 'pages/blog'),
|
||||||
'white-papers' => menuItem('livres-blancs', 'Livres blancs', 'book', 'pages/livres-blancs'),
|
'white-papers' => menuItem('livres-blancs', 'Livres blancs', 'book', 'pages/livres-blancs'),
|
||||||
|
'confidentialite' => menuItem('confidentialite', 'Confidentialité', 'lock', 'pages/confidentialite'),
|
||||||
'-',
|
'-',
|
||||||
'users',
|
'users',
|
||||||
'system',
|
'system',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
$defaultLang = kirby()->defaultLanguage()->code();
|
||||||
|
|
||||||
$specificData = [
|
$specificData = [
|
||||||
'backgroundImage' => $page->backgroundImage()->toFile()?->url(),
|
'backgroundImage' => $page->backgroundImage()->toFile()?->url(),
|
||||||
'projects' => $page->children()->listed()->map(function($project) {
|
'projects' => $page->children()->listed()->map(function($project) use ($defaultLang) {
|
||||||
|
$mockupFile = $project->content($defaultLang)->mockup()->toFile();
|
||||||
return [
|
return [
|
||||||
'title' => $project->title()->value(),
|
'title' => $project->title()->value(),
|
||||||
'slug' => $project->slug(),
|
'slug' => $project->slug(),
|
||||||
|
|
@ -14,9 +17,9 @@ $specificData = [
|
||||||
'srcset' => $f->srcset('gallery'),
|
'srcset' => $f->srcset('gallery'),
|
||||||
'webp' => $f->srcset('gallery-webp'),
|
'webp' => $f->srcset('gallery-webp'),
|
||||||
])->values(),
|
])->values(),
|
||||||
'mockup' => $project->content(kirby()->defaultLanguage()->code())->mockup()->toFile()?->url(),
|
'mockup' => $mockupFile?->url(),
|
||||||
'mockupSrcset' => $project->content(kirby()->defaultLanguage()->code())->mockup()->toFile()?->srcset('mockup'),
|
'mockupSrcset' => $mockupFile?->srcset('mockup'),
|
||||||
'mockupWebp' => $project->content(kirby()->defaultLanguage()->code())->mockup()->toFile()?->srcset('mockup-webp'),
|
'mockupWebp' => $mockupFile?->srcset('mockup-webp'),
|
||||||
'galleryAnimationMode' => $project->galleryAnimationMode()->value() ?: 'vertical',
|
'galleryAnimationMode' => $project->galleryAnimationMode()->value() ?: 'vertical',
|
||||||
'secondsPerImage' => $project->secondsPerImage()->isNotEmpty() ? (int) $project->secondsPerImage()->value() : 8,
|
'secondsPerImage' => $project->secondsPerImage()->isNotEmpty() ? (int) $project->secondsPerImage()->value() : 8,
|
||||||
'galleryBackgroundColor' => $project->galleryBackgroundColor()->value(),
|
'galleryBackgroundColor' => $project->galleryBackgroundColor()->value(),
|
||||||
|
|
|
||||||
21
site/templates/privacy.json.php
Normal file
21
site/templates/privacy.json.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$bodyBlocks = [];
|
||||||
|
foreach ($page->body()->toBlocks() as $block) {
|
||||||
|
if ($block->type() === 'text') {
|
||||||
|
$bodyBlocks[] = [
|
||||||
|
'type' => 'text',
|
||||||
|
'html' => $block->content()->text()->value(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$specificData = [
|
||||||
|
'backgroundImage' => $page->backgroundImage()->toFile()?->url(),
|
||||||
|
'body' => $bodyBlocks,
|
||||||
|
];
|
||||||
|
|
||||||
|
$pageData = array_merge($genericData, $specificData);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($pageData);
|
||||||
2
site/templates/privacy.php
Normal file
2
site/templates/privacy.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php snippet('header') ?>
|
||||||
|
<?php snippet('footer') ?>
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import Blog from '@views/Blog.svelte'
|
import Blog from '@views/Blog.svelte'
|
||||||
import Article from '@views/Article.svelte'
|
import Article from '@views/Article.svelte'
|
||||||
import WhitePapers from '@views/WhitePapers.svelte'
|
import WhitePapers from '@views/WhitePapers.svelte'
|
||||||
|
import Privacy from '@views/Privacy.svelte'
|
||||||
import Default from '@views/Default.svelte'
|
import Default from '@views/Default.svelte'
|
||||||
|
|
||||||
const templates = {
|
const templates = {
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
blog: Blog,
|
blog: Blog,
|
||||||
article: Article,
|
article: Article,
|
||||||
'white-papers': WhitePapers,
|
'white-papers': WhitePapers,
|
||||||
|
privacy: Privacy,
|
||||||
default: Default
|
default: Default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,13 +39,15 @@
|
||||||
const wrapperTransform = $derived(`translateX(-${slides.activeIndex * 100}vw)`)
|
const wrapperTransform = $derived(`translateX(-${slides.activeIndex * 100}vw)`)
|
||||||
|
|
||||||
const linesBySlide = {
|
const linesBySlide = {
|
||||||
home: [6, 11, 16],
|
home: [6, 11, 16],
|
||||||
expertise: [6, 8, 11, 14, 16],
|
expertise: [6, 8, 11, 14, 16],
|
||||||
about: [6, 8, 11, 14, 16],
|
about: [6, 8, 11, 14, 16],
|
||||||
portfolio: [11, 16],
|
portfolio: [11, 16],
|
||||||
|
privacy: [6, 8, 11, 14, 16],
|
||||||
}
|
}
|
||||||
const ALL_COLS = [6, 8, 11, 14, 16]
|
const ALL_COLS = [6, 8, 11, 14, 16]
|
||||||
const activeLines = $derived(new Set(linesBySlide[slides.active?.template] ?? []))
|
const activeTemplate = $derived(slides.standalone?.template ?? slides.active?.template)
|
||||||
|
const activeLines = $derived(new Set(linesBySlide[activeTemplate] ?? []))
|
||||||
|
|
||||||
let isReady = $state(false)
|
let isReady = $state(false)
|
||||||
let isResizing = $state(false)
|
let isResizing = $state(false)
|
||||||
|
|
@ -143,22 +147,29 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div
|
{#if slides.standalone}
|
||||||
class="slides-wrapper"
|
<svelte:component
|
||||||
class:is-animated={isReady && !isResizing}
|
this={templates[slides.standalone.template] ?? Default}
|
||||||
style="width: {wrapperWidth}; transform: {wrapperTransform}"
|
data={slides.standalone}
|
||||||
>
|
/>
|
||||||
{#each slides.all as slide, i}
|
{:else}
|
||||||
<section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id} inert={i !== slides.activeIndex}>
|
<div
|
||||||
{#if slide.loaded}
|
class="slides-wrapper"
|
||||||
<svelte:component
|
class:is-animated={isReady && !isResizing}
|
||||||
this={templates[slide.template] ?? Default}
|
style="width: {wrapperWidth}; transform: {wrapperTransform}"
|
||||||
data={slide.data}
|
>
|
||||||
/>
|
{#each slides.all as slide, i}
|
||||||
{/if}
|
<section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id} inert={i !== slides.activeIndex}>
|
||||||
</section>
|
{#if slide.loaded}
|
||||||
{/each}
|
<svelte:component
|
||||||
</div>
|
this={templates[slide.template] ?? Default}
|
||||||
|
data={slide.data}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { site } from '@state/site.svelte'
|
import { site } from '@state/site.svelte'
|
||||||
|
import { locale } from '@state/locale.svelte'
|
||||||
import { t } from '@i18n'
|
import { t } from '@i18n'
|
||||||
|
|
||||||
const logo = $derived(site.logo)
|
const logo = $derived(site.logo)
|
||||||
|
|
@ -89,9 +90,9 @@
|
||||||
<div class="footer-divider" aria-hidden="true"></div>
|
<div class="footer-divider" aria-hidden="true"></div>
|
||||||
{#if contact.legalNotice}
|
{#if contact.legalNotice}
|
||||||
<a href={contact.legalNotice} target="_blank" rel="noopener noreferrer">{t('legal')}</a>
|
<a href={contact.legalNotice} target="_blank" rel="noopener noreferrer">{t('legal')}</a>
|
||||||
{:else}
|
|
||||||
<a href="/privacy">{t('privacy')}</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="footer-divider" aria-hidden="true"></div>
|
||||||
|
<a href={locale.current === 'en' ? '/en/confidentialite' : '/confidentialite'}>{t('privacy')}</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,12 @@ async function loadSlide(path) {
|
||||||
siteInitialized = true;
|
siteInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
slides.setData(slidePath, data);
|
const finalIdx = slides.getIndexByPath(slidePath);
|
||||||
|
if (finalIdx !== -1) {
|
||||||
|
slides.setData(slidePath, data);
|
||||||
|
} else {
|
||||||
|
slides.setStandalone(data);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[router] Failed to load slide ${slidePath}:`, error);
|
console.error(`[router] Failed to load slide ${slidePath}:`, error);
|
||||||
slides.setLoading(slidePath, false);
|
slides.setLoading(slidePath, false);
|
||||||
|
|
@ -95,21 +100,26 @@ export function slideTo(path, { skipHistory = false } = {}) {
|
||||||
const idx = findSlideIndex(path);
|
const idx = findSlideIndex(path);
|
||||||
const slidePath = idx !== -1 ? slides.all[idx].path : path;
|
const slidePath = idx !== -1 ? slides.all[idx].path : path;
|
||||||
|
|
||||||
if (idx !== -1 && slides.all[idx].title) {
|
if (idx !== -1) {
|
||||||
document.title = `World Game - ${slides.all[idx].title}`;
|
slides.clearStandalone();
|
||||||
}
|
|
||||||
|
|
||||||
// Si on navigue vers la slide déjà active (ex: clic sur "Blog" depuis un article),
|
if (slides.all[idx].title) {
|
||||||
// déclencher popstate pour que la vue puisse réagir au changement d'URL.
|
document.title = `World Game - ${slides.all[idx].title}`;
|
||||||
if (idx === slides.activeIndex && !skipHistory) {
|
}
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
slides.slideTo(slidePath);
|
if (idx === slides.activeIndex && !skipHistory) {
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (idx !== -1 && !slides.all[idx].loaded) {
|
slides.slideTo(slidePath);
|
||||||
loadSlide(slidePath);
|
|
||||||
|
if (!slides.all[idx].loaded) {
|
||||||
|
loadSlide(slidePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Page standalone (hors navigation)
|
||||||
|
loadSlide(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
let slidesData = $state([])
|
let slidesData = $state([])
|
||||||
let activeIndex = $state(0)
|
let activeIndex = $state(0)
|
||||||
let pendingPath = $state(null)
|
let pendingPath = $state(null)
|
||||||
|
let standaloneData = $state(null)
|
||||||
|
|
||||||
function getIndexByPath(path) {
|
function getIndexByPath(path) {
|
||||||
return slidesData.findIndex(s => s.path === path)
|
return slidesData.findIndex(s => s.path === path)
|
||||||
|
|
@ -11,6 +12,10 @@ export const slides = {
|
||||||
get activeIndex() { return activeIndex },
|
get activeIndex() { return activeIndex },
|
||||||
get active() { return slidesData[activeIndex] ?? null },
|
get active() { return slidesData[activeIndex] ?? null },
|
||||||
get pendingPath() { return pendingPath },
|
get pendingPath() { return pendingPath },
|
||||||
|
get standalone() { return standaloneData },
|
||||||
|
|
||||||
|
setStandalone(data) { standaloneData = data },
|
||||||
|
clearStandalone() { standaloneData = null },
|
||||||
|
|
||||||
init(siteNavigation) {
|
init(siteNavigation) {
|
||||||
slidesData = siteNavigation.map(nav => ({
|
slidesData = siteNavigation.map(nav => ({
|
||||||
|
|
|
||||||
123
src/views/Privacy.svelte
Normal file
123
src/views/Privacy.svelte
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<script>
|
||||||
|
import { navigation } from '@state/navigation.svelte'
|
||||||
|
import Footer from '@components/layout/Footer.svelte'
|
||||||
|
|
||||||
|
let { data } = $props()
|
||||||
|
|
||||||
|
const body = $derived(data?.body ?? [])
|
||||||
|
const backgroundImage = $derived(data?.backgroundImage ?? null)
|
||||||
|
|
||||||
|
let sectionEl = $state(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="privacy golden-grid page-scrollable"
|
||||||
|
style={backgroundImage ? `--background-image: url('${backgroundImage}')` : ''}
|
||||||
|
bind:this={sectionEl}
|
||||||
|
onscroll={() => navigation.setScrolled(sectionEl.scrollTop > 100)}
|
||||||
|
>
|
||||||
|
<div class="page-container">
|
||||||
|
{#if body.length > 0}
|
||||||
|
<section class="privacy-body">
|
||||||
|
{#each body as block}
|
||||||
|
{#if block.type === 'text'}
|
||||||
|
<div class="privacy-body-block">
|
||||||
|
{@html block.html}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.privacy {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #fff;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-image: var(--background-image, none);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
grid-area: 6/6 / span 7 / span 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
place-self: center;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Body blocks ── */
|
||||||
|
.privacy-body {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block {
|
||||||
|
border-left: 2px solid #04fea0;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-top: 3rem;
|
||||||
|
text-align: left;
|
||||||
|
font-family: "Danzza", sans-serif;
|
||||||
|
font-size: var(--font-size-paragraph);
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: pre-line;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block :global(h3) {
|
||||||
|
font-family: Danzza Medium, sans-serif;
|
||||||
|
font-size: var(--font-size-paragraph);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block :global(h3::before) {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: .6rem;
|
||||||
|
height: .6rem;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
margin-right: .6rem;
|
||||||
|
margin-bottom: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block :global(p) {
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block :global(a) {
|
||||||
|
color: #04fea0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile (≤700px) ── */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.page-container {
|
||||||
|
grid-area: auto;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body {
|
||||||
|
padding: 6rem 1.25rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body-block {
|
||||||
|
font-size: var(--font-size-paragraph-small, 0.875rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tablet (701px–912px) ── */
|
||||||
|
@media (min-width: 701px) and (max-width: 912px) {
|
||||||
|
.privacy-body-block {
|
||||||
|
font-size: var(--font-size-paragraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -375,15 +375,6 @@
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
scale: 1.5;
|
scale: 1.5;
|
||||||
|
|
||||||
/* position: absolute;
|
|
||||||
bottom: -10%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 75%;
|
|
||||||
max-width: 300px;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Quand le formulaire est affiché : masque le contenu principal */
|
/* Quand le formulaire est affiché : masque le contenu principal */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue