Compare commits

...

3 commits

Author SHA1 Message Date
isUnknown
9ab6344835 Renommer lib/ en state/ pour plus de clarté
All checks were successful
Deploy / Deploy to Production (push) Successful in 13s
- Renommage du dossier src/lib/ en src/state/
- Mise à jour de l'alias @lib vers @state dans vite.config.js
- Suppression de l'alias @stores devenu obsolète
- Mise à jour de tous les imports dans les composants et vues

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 08:26:28 +01:00
isUnknown
a12c2df8f9 Upgrade vers Svelte 5 et reproduction de la page d'accueil
- Upgrade : Svelte 5.0.0, Vite 7.0.4, @sveltejs/vite-plugin-svelte 6.0.0
- Migration syntaxe Svelte 5 : $derived, $props(), onclick, mount()
- Navbar identique au site source avec logo GIF et menu animé
- Page Home avec vidéo plein écran et lignes verticales
- CSS modulaire organisé en fichiers séparés (variables, fonts, layout, buttons, etc.)
- Assets copiés : fonts Danzza, vidéos, icônes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 07:52:16 +01:00
isUnknown
cbe89acb21 Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod

Architecture fonctionnelle de base établie, à améliorer et compléter.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
74 changed files with 4038 additions and 772 deletions

15
.gitignore vendored
View file

@ -57,4 +57,17 @@ Icon
# Claude settings
# ---------------
/.claude
/.claude
# Node.js
# ---------------
node_modules/
npm-debug.log*
*.log
# Build
# ---------------
dist/
node_modules/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Binary file not shown.

46
index.html Normal file
View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Game - Play to Engage</title>
<meta name="description" content="World Game - Création de jeux et expériences interactives">
<!-- This file is only used during development with Vite -->
<!-- In production, Kirby's header.php will be used -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- Mock Kirby data for development -->
<script>
window.__KIRBY__ = {
page: {
id: 'home',
template: 'home',
url: '/',
uri: ''
},
site: {
title: 'World Game',
url: 'http://localhost:8000',
language: 'fr',
languages: [
{ code: 'fr', name: 'Français' },
{ code: 'en', name: 'English' }
],
logo: null,
navigation: [
{ label_fr: 'Accueil', label_en: 'Home', url: '/', isActive: true },
{ label_fr: 'Expertise', label_en: 'Expertise', url: '/expertise', isActive: false },
{ label_fr: 'Portfolio', label_en: 'Portfolio', url: '/portfolio', isActive: false },
{ label_fr: 'Jouer', label_en: 'Play', url: '/jouer', isActive: false },
{ label_fr: 'À propos', label_en: 'About', url: '/a-propos', isActive: false },
{ label_fr: 'Blog', label_en: 'Blog', url: '/blog', isActive: false }
]
}
};
</script>
</body>
</html>

1385
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "world-game-website",
"version": "1.0.0",
"description": "World Game - Svelte + Kirby",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"kirby:dev": "php -S localhost:8000"
},
"repository": {
"type": "git",
"url": "https://forge.studio-variable.com/studio-variable/world-game.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@sveltejs/vite-plugin-svelte": "6.0.0",
"gsap": "^3.14.2",
"navaid": "^1.2.0",
"svelte": "5.0.0",
"vite": "7.0.4"
},
"devDependencies": {
"fs-extra": "^11.3.3"
}
}

37
site/controllers/site.php Normal file
View file

@ -0,0 +1,37 @@
<?php
return function ($page, $kirby, $site) {
// Generic page data available in all templates
$genericData = [
'title' => $page->title()->value(),
'url' => $page->url(),
'uri' => $page->uri(),
'template' => $page->intendedTemplate()->name(),
'modified' => $page->modified('Y-m-d'),
'site' => [
'title' => $site->site_title()->value(),
'url' => $site->url(),
'logo' => $site->logo()->toFile()?->url(),
'language' => $kirby->language()?->code() ?? 'fr',
'languages' => $kirby->languages()->map(function($l) {
return [
'code' => $l->code(),
'name' => $l->name()
];
})->values(),
'navigation' => $site->main_navigation()->toStructure()->map(function($item) use ($kirby) {
$linkedPage = $item->link()->toPages()->first();
return [
'label_fr' => $item->label_fr()->value(),
'label_en' => $item->label_en()->value(),
'url' => $linkedPage?->url(),
'isActive' => $linkedPage?->isOpen() ?? false
];
})->values()
]
];
return [
'genericData' => $genericData
];
};

View file

@ -1,82 +1,2 @@
<!-- Footer -->
<footer class="site-footer">
<div class="site-footer__container">
<!-- Logo & Info -->
<div class="site-footer__brand">
<?php if ($logo = $site->logo()->toFile()): ?>
<img src="<?= $logo->url() ?>" alt="<?= $site->site_title() ?>" class="site-footer__logo">
<?php else: ?>
<span class="site-footer__logo-text"><?= $site->site_title() ?></span>
<?php endif ?>
<?php if ($site->site_tagline()->isNotEmpty()): ?>
<p class="site-footer__tagline"><?= $site->site_tagline() ?></p>
<?php endif ?>
</div>
<!-- Contact -->
<div class="site-footer__contact">
<?php if ($site->contact_email()->isNotEmpty()): ?>
<a href="mailto:<?= $site->contact_email() ?>" class="site-footer__email">
<?= $site->contact_email() ?>
</a>
<?php endif ?>
<?php if ($site->contact_phone()->isNotEmpty()): ?>
<a href="tel:<?= $site->contact_phone() ?>" class="site-footer__phone">
<?= $site->contact_phone() ?>
</a>
<?php endif ?>
<?php if ($site->contact_address()->isNotEmpty()): ?>
<address class="site-footer__address">
<?= $site->contact_address()->kt() ?>
</address>
<?php endif ?>
</div>
<!-- Social Links -->
<div class="site-footer__social">
<?php if ($site->social_linkedin()->isNotEmpty()): ?>
<a href="<?= $site->social_linkedin() ?>" class="site-footer__social-link" target="_blank" rel="noopener noreferrer">
LinkedIn
</a>
<?php endif ?>
<?php if ($site->social_twitter()->isNotEmpty()): ?>
<a href="<?= $site->social_twitter() ?>" class="site-footer__social-link" target="_blank" rel="noopener noreferrer">
Twitter
</a>
<?php endif ?>
<?php if ($site->social_instagram()->isNotEmpty()): ?>
<a href="<?= $site->social_instagram() ?>" class="site-footer__social-link" target="_blank" rel="noopener noreferrer">
Instagram
</a>
<?php endif ?>
<?php if ($site->social_youtube()->isNotEmpty()): ?>
<a href="<?= $site->social_youtube() ?>" class="site-footer__social-link" target="_blank" rel="noopener noreferrer">
YouTube
</a>
<?php endif ?>
</div>
<!-- Mentions légales -->
<?php if ($pdf = $site->mentions_legales()->toFile()): ?>
<div class="site-footer__legal">
<a href="<?= $pdf->url() ?>" target="_blank">Mentions légales</a>
</div>
<?php endif ?>
<!-- Copyright -->
<div class="site-footer__copyright">
<p>&copy; <?= date('Y') ?> <?= $site->site_title() ?>. Tous droits réservés.</p>
</div>
</div>
</footer>
<!-- Scripts -->
<?= js('assets/js/main.js') ?>
</body>
</html>

View file

@ -10,9 +10,6 @@
<!-- Favicon -->
<link rel="icon" type="image/png" href="<?= url('assets/favicon.png') ?>">
<!-- Styles -->
<?= css('assets/css/main.css') ?>
<!-- Open Graph -->
<meta property="og:title" content="<?= $page->meta_title()->or($page->title()) ?>">
<meta property="og:description" content="<?= $page->meta_description()->or($site->site_description()) ?>">
@ -21,53 +18,16 @@
<?php if ($cover = $page->cover()->toFile()): ?>
<meta property="og:image" content="<?= $cover->url() ?>">
<?php endif ?>
<?php if (file_exists('assets/dist')): ?>
<!-- Production: Load compiled assets -->
<script type="module" src="<?= url('assets/dist/index.js') ?>" defer></script>
<link rel="stylesheet" href="<?= url('assets/dist/index.css') ?>">
<?php else: ?>
<!-- Development: Load from Vite dev server -->
<script type="module" src="http://localhost:5173/@vite/client" defer></script>
<script type="module" src="http://localhost:5173/src/main.js" defer></script>
<?php endif ?>
</head>
<body class="template-<?= $page->template() ?>">
<!-- Header -->
<header class="site-header">
<div class="site-header__container">
<!-- Logo -->
<a href="<?= $site->url() ?>" class="site-header__logo">
<?php if ($logo = $site->logo()->toFile()): ?>
<img src="<?= $logo->url() ?>" alt="<?= $site->site_title() ?>">
<?php else: ?>
<span class="site-header__logo-text"><?= $site->site_title() ?></span>
<?php endif ?>
</a>
<!-- Navigation -->
<nav class="site-header__nav">
<ul class="site-nav">
<?php foreach ($site->main_navigation()->toStructure() as $item): ?>
<?php $linkedPage = $item->link()->toPages()->first() ?>
<?php if ($linkedPage): ?>
<li class="site-nav__item <?= $linkedPage->isOpen() ? 'is-active' : '' ?>">
<a href="<?= $linkedPage->url() ?>">
<?= $kirby->language()?->code() === 'en' ? $item->label_en() : $item->label_fr() ?>
</a>
</li>
<?php endif ?>
<?php endforeach ?>
</ul>
</nav>
<!-- Language Switcher -->
<?php if ($kirby->languages()->count() > 1): ?>
<div class="site-header__lang">
<?php foreach ($kirby->languages() as $lang): ?>
<a href="<?= $page->url($lang->code()) ?>"
class="site-lang__link <?= $lang->code() === $kirby->language()?->code() ? 'is-active' : '' ?>">
<?= strtoupper($lang->code()) ?>
</a>
<?php endforeach ?>
</div>
<?php endif ?>
<!-- Mobile Menu Toggle -->
<button class="site-header__toggle" aria-label="Menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
</header>
<body>
<div id="app"></div>

View file

@ -0,0 +1,34 @@
<?php
$specificData = [
'intro' => [
'title' => $page->intro_title()->value(),
'text' => $page->intro_text()->value()
],
'mission' => [
'title' => $page->mission_title()->value(),
'text' => $page->mission_text()->toBlocks()
],
'manifesto' => [
'title' => $page->manifesto_title()->value(),
'text' => $page->manifesto_text()->toBlocks()
],
'team' => [
'title' => $page->team_title()->value(),
'members' => $page->team_members()->toStructure()->map(function($member) {
return [
'name' => $member->name()->value(),
'role' => $member->role()->value(),
'bio' => $member->bio()->value(),
'photo' => $member->photo()->toFile()?->url(),
'linkedin' => $member->linkedin()->value(),
'twitter' => $member->twitter()->value()
];
})->values()
]
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,81 +1,2 @@
<?php snippet('header') ?>
<main class="about">
<!-- Intro Section -->
<section class="about__intro">
<h1 class="about__title"><?= $page->intro_title() ?></h1>
<?php if ($page->intro_text()->isNotEmpty()): ?>
<p class="about__subtitle"><?= $page->intro_text() ?></p>
<?php endif ?>
</section>
<!-- Mission Section -->
<?php if ($page->mission_text()->isNotEmpty()): ?>
<section class="about__mission">
<h2 class="about__section-title"><?= $page->mission_title() ?></h2>
<div class="about__section-content">
<?= $page->mission_text()->toBlocks() ?>
</div>
</section>
<?php endif ?>
<!-- Manifesto Section -->
<?php if ($page->manifesto_text()->isNotEmpty()): ?>
<section class="about__manifesto">
<h2 class="about__section-title"><?= $page->manifesto_title() ?></h2>
<div class="about__section-content">
<?= $page->manifesto_text()->toBlocks() ?>
</div>
</section>
<?php endif ?>
<!-- Team Section -->
<?php if ($page->team_members()->isNotEmpty()): ?>
<section class="about__team">
<h2 class="about__section-title"><?= $page->team_title() ?></h2>
<div class="about__team-grid">
<?php foreach ($page->team_members()->toStructure() as $member): ?>
<article class="team-card">
<?php if ($photo = $member->photo()->toFile()): ?>
<div class="team-card__photo">
<img src="<?= $photo->url() ?>" alt="<?= $member->name() ?>">
</div>
<?php endif ?>
<div class="team-card__info">
<h3 class="team-card__name"><?= $member->name() ?></h3>
<p class="team-card__role"><?= $member->role() ?></p>
<?php if ($member->bio()->isNotEmpty()): ?>
<p class="team-card__bio"><?= $member->bio() ?></p>
<?php endif ?>
<div class="team-card__social">
<?php if ($member->linkedin()->isNotEmpty()): ?>
<a href="<?= $member->linkedin() ?>" target="_blank" class="team-card__social-link">
LinkedIn
</a>
<?php endif ?>
<?php if ($member->twitter()->isNotEmpty()): ?>
<a href="<?= $member->twitter() ?>" target="_blank" class="team-card__social-link">
Twitter
</a>
<?php endif ?>
</div>
</div>
</article>
<?php endforeach ?>
</div>
<nav class="about__team-nav">
<button class="about__team-prev">&larr; Précédent</button>
<button class="about__team-next">Suivant &rarr;</button>
</nav>
</section>
<?php endif ?>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,34 @@
<?php
$related = $page->related_articles()->toPages();
if ($related->isEmpty()) {
$related = $page->siblings()->listed()->not($page)->shuffle()->limit(3);
}
$specificData = [
'date' => $page->date()->toDate('Y-m-d'),
'date_formatted' => $page->date()->toDate('d/m/Y'),
'intro' => $page->intro()->value(),
'author' => [
'name' => $page->author_name()->value(),
'role' => $page->author_role()->value(),
'photo' => $page->author_photo()->toFile()?->url()
],
'cover' => $page->cover()->toFile()?->url(),
'content' => $page->article_content()->toBlocks(),
'tags' => $page->tags()->split(),
'related' => $related->map(function($rec) {
return [
'title' => $rec->title()->value(),
'url' => $rec->url(),
'category' => $rec->category()->value(),
'cover' => $rec->cover()->toFile()?->thumb(['width' => 400])->url()
];
})->values(),
'parent_url' => $page->parent()->url()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,101 +1,2 @@
<?php snippet('header') ?>
<main class="article">
<article class="article__content">
<!-- Header -->
<header class="article__header">
<?php if ($page->date()->isNotEmpty()): ?>
<time class="article__date" datetime="<?= $page->date()->toDate('Y-m-d') ?>">
Publié le <?= $page->date()->toDate('d/m/Y') ?>
</time>
<?php endif ?>
<h1 class="article__title"><?= $page->title() ?></h1>
<?php if ($page->intro()->isNotEmpty()): ?>
<p class="article__intro"><?= $page->intro() ?></p>
<?php endif ?>
</header>
<!-- Author -->
<?php if ($page->author_name()->isNotEmpty()): ?>
<div class="article__author">
<?php if ($photo = $page->author_photo()->toFile()): ?>
<img src="<?= $photo->url() ?>" alt="<?= $page->author_name() ?>" class="article__author-photo">
<?php endif ?>
<div class="article__author-info">
<span class="article__author-name"><?= $page->author_name() ?></span>
<?php if ($page->author_role()->isNotEmpty()): ?>
<span class="article__author-role"><?= $page->author_role() ?></span>
<?php endif ?>
</div>
</div>
<?php endif ?>
<!-- Cover Image -->
<?php if ($cover = $page->cover()->toFile()): ?>
<figure class="article__cover">
<img src="<?= $cover->url() ?>" alt="<?= $page->title() ?>">
</figure>
<?php endif ?>
<!-- Content -->
<div class="article__body">
<?= $page->article_content()->toBlocks() ?>
</div>
<!-- Tags -->
<?php if ($page->tags()->isNotEmpty()): ?>
<div class="article__tags">
<?php foreach ($page->tags()->split() as $tag): ?>
<span class="article__tag"><?= $tag ?></span>
<?php endforeach ?>
</div>
<?php endif ?>
</article>
<!-- Recommendations -->
<?php
$related = $page->related_articles()->toPages();
if ($related->isEmpty()) {
$related = $page->siblings()->listed()->not($page)->shuffle()->limit(3);
}
?>
<?php if ($related->isNotEmpty()): ?>
<section class="article__recommendations">
<h2 class="article__recommendations-title">Nos recommandations</h2>
<a href="<?= $page->parent()->url() ?>" class="article__recommendations-link">Voir tous les articles &rarr;</a>
<div class="article__recommendations-grid">
<?php foreach ($related as $rec): ?>
<article class="recommendation-card">
<?php if ($cover = $rec->cover()->toFile()): ?>
<div class="recommendation-card__cover">
<img src="<?= $cover->thumb(['width' => 400])->url() ?>" alt="<?= $rec->title() ?>">
</div>
<?php endif ?>
<?php if ($rec->category()->isNotEmpty()): ?>
<span class="recommendation-card__category"><?= $rec->category() ?></span>
<?php endif ?>
<h3 class="recommendation-card__title">
<a href="<?= $rec->url() ?>"><?= $rec->title() ?></a>
</h3>
</article>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
<!-- Navigation -->
<nav class="article__nav">
<a href="<?= $page->parent()->url() ?>" class="article__nav-back">
&larr; Tous les articles
</a>
</nav>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,26 @@
<?php
$specificData = [
'intro' => [
'title' => $page->intro_title()->value(),
'text' => $page->intro_text()->value()
],
'articles' => $page->children()->listed()->sortBy('date', 'desc')->map(function($article) {
return [
'title' => $article->title()->value(),
'slug' => $article->slug(),
'url' => $article->url(),
'date' => $article->date()->toDate('Y-m-d'),
'date_formatted' => $article->date()->toDate('d/m/Y'),
'intro' => $article->intro()->excerpt(200),
'cover' => $article->cover()->toFile()?->url(),
'author_name' => $article->author_name()->value(),
'author_photo' => $article->author_photo()->toFile()?->url()
];
})->values()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,61 +1,2 @@
<?php snippet('header') ?>
<main class="blog">
<!-- Intro Section -->
<section class="blog__intro">
<h1 class="blog__title"><?= $page->intro_title() ?></h1>
<?php if ($page->intro_text()->isNotEmpty()): ?>
<p class="blog__subtitle"><?= $page->intro_text() ?></p>
<?php endif ?>
</section>
<!-- Articles List -->
<section class="blog__articles">
<?php foreach ($page->children()->listed()->sortBy('date', 'desc') as $article): ?>
<article class="article-card">
<!-- Date -->
<?php if ($article->date()->isNotEmpty()): ?>
<time class="article-card__date" datetime="<?= $article->date()->toDate('Y-m-d') ?>">
<?= $article->date()->toDate('d/m/Y') ?>
</time>
<?php endif ?>
<!-- Content -->
<div class="article-card__content">
<h2 class="article-card__title">
<a href="<?= $article->url() ?>"><?= $article->title() ?></a>
</h2>
<?php if ($article->intro()->isNotEmpty()): ?>
<p class="article-card__intro"><?= $article->intro()->excerpt(200) ?></p>
<?php endif ?>
<a href="<?= $article->url() ?>" class="article-card__link">
Lire la suite &rarr;
</a>
</div>
<!-- Author -->
<div class="article-card__author">
<?php if ($photo = $article->author_photo()->toFile()): ?>
<img src="<?= $photo->url() ?>" alt="<?= $article->author_name() ?>" class="article-card__author-photo">
<?php endif ?>
<?php if ($article->author_name()->isNotEmpty()): ?>
<span class="article-card__author-name"><?= $article->author_name() ?></span>
<?php endif ?>
</div>
<!-- Cover Image -->
<?php if ($cover = $article->cover()->toFile()): ?>
<div class="article-card__cover">
<img src="<?= $cover->url() ?>" alt="<?= $article->title() ?>">
</div>
<?php endif ?>
</article>
<?php endforeach ?>
</section>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,10 @@
<?php
$specificData = [
'body' => $page->body()->toBlocks()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1 +1,2 @@
<h1><?= $page->title() ?></h1>
<?php snippet('header') ?>
<?php snippet('footer') ?>

View file

@ -0,0 +1,24 @@
<?php
$specificData = [
'intro' => [
'title' => $page->intro_title()->value(),
'text' => $page->intro_text()->value()
],
'sections' => $page->expertise_sections()->toStructure()->map(function($section) {
return [
'title' => $section->title()->value(),
'icon' => $section->icon()->value(),
'content' => $section->content()->toBlocks()
];
})->values(),
'objective' => [
'title' => $page->objective_title()->value(),
'text' => $page->objective_text()->value()
]
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,36 +1,2 @@
<?php snippet('header') ?>
<main class="expertise">
<!-- Intro Section -->
<section class="expertise__intro">
<h1 class="expertise__title"><?= $page->intro_title() ?></h1>
<?php if ($page->intro_text()->isNotEmpty()): ?>
<p class="expertise__text"><?= $page->intro_text() ?></p>
<?php endif ?>
</section>
<!-- Expertise Sections -->
<?php if ($page->expertise_sections()->isNotEmpty()): ?>
<div class="expertise__sections">
<?php foreach ($page->expertise_sections()->toStructure() as $section): ?>
<section class="expertise-section expertise-section--<?= $section->icon() ?>">
<h2 class="expertise-section__title"><?= $section->title() ?></h2>
<div class="expertise-section__content">
<?= $section->content()->toBlocks() ?>
</div>
</section>
<?php endforeach ?>
</div>
<?php endif ?>
<!-- Objective Section -->
<?php if ($page->objective_text()->isNotEmpty()): ?>
<section class="expertise__objective">
<h2 class="expertise__objective-title"><?= $page->objective_title() ?></h2>
<p class="expertise__objective-text"><?= $page->objective_text() ?></p>
</section>
<?php endif ?>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,16 @@
<?php
$specificData = [
'description' => $page->description()->value(),
'rules' => $page->rules()->toBlocks(),
'game_status' => $page->game_status()->value(),
'is_embedded' => $page->is_embedded()->toBool(),
'play_link' => $page->play_link()->value(),
'cover' => $page->cover()->toFile()?->url(),
'parent_url' => $page->parent()->url()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,58 +1,2 @@
<?php snippet('header') ?>
<main class="game">
<article class="game__content">
<!-- Info -->
<div class="game__info">
<h1 class="game__title"><?= $page->title() ?></h1>
<?php if ($page->description()->isNotEmpty()): ?>
<p class="game__description"><?= $page->description() ?></p>
<?php endif ?>
<!-- Rules -->
<?php if ($page->rules()->isNotEmpty()): ?>
<div class="game__rules">
<?= $page->rules()->toBlocks() ?>
</div>
<?php endif ?>
<!-- Status Button -->
<?php if ($page->game_status()->value() !== 'available'): ?>
<div class="game__status">
<span class="btn btn--disabled">
<?= $page->game_status()->value() === 'coming_soon' ? 'Bientôt disponible' : 'En maintenance' ?>
</span>
</div>
<?php endif ?>
</div>
<!-- Game Area -->
<div class="game__area">
<?php if ($page->game_status()->value() === 'available'): ?>
<?php if ($page->is_embedded()->toBool() && $page->play_link()->isNotEmpty()): ?>
<iframe src="<?= $page->play_link() ?>" class="game__iframe" frameborder="0"></iframe>
<?php elseif ($page->play_link()->isNotEmpty()): ?>
<a href="<?= $page->play_link() ?>" class="game__external-link btn btn--large" target="_blank">
Jouer maintenant
</a>
<?php endif ?>
<?php if ($cover = $page->cover()->toFile()): ?>
<div class="game__cover">
<img src="<?= $cover->url() ?>" alt="<?= $page->title() ?>">
</div>
<?php endif ?>
<?php endif ?>
</div>
</article>
<!-- Back Navigation -->
<nav class="game__nav">
<a href="<?= $page->parent()->url() ?>" class="game__nav-back">
&larr; Retour aux jeux
</a>
</nav>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,24 @@
<?php
$specificData = [
'hero' => [
'title' => $page->hero_title()->value(),
'title_highlight' => $page->hero_title_highlight()->value(),
'subtitle' => $page->hero_subtitle()->value(),
'cta_text' => $page->hero_cta_text()->value(),
'cta_link' => $page->hero_cta_link()->toPage()?->url() ?? '#',
'image' => $page->hero_image()->toFile()?->url()
],
'background_video' => $page->background_video()->toFile()?->url(),
'floating_bubbles' => $page->floating_bubbles()->toStructure()->map(function($bubble) {
return [
'text' => $bubble->text()->value(),
'position' => $bubble->position()->value()
];
})->values()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,49 +1,2 @@
<?php snippet('header') ?>
<main class="home">
<!-- Hero Section -->
<section class="hero">
<div class="hero__content">
<h1 class="hero__title">
<?php
$title = $page->hero_title()->value();
$highlight = $page->hero_title_highlight()->value();
if ($highlight) {
echo str_replace($highlight, '<span class="highlight">' . $highlight . '</span>', $title);
} else {
echo $title;
}
?>
</h1>
<?php if ($page->hero_subtitle()->isNotEmpty()): ?>
<p class="hero__subtitle"><?= $page->hero_subtitle() ?></p>
<?php endif ?>
<?php if ($page->hero_cta_text()->isNotEmpty()): ?>
<a href="<?= $page->hero_cta_link()->toPage()?->url() ?? '#' ?>" class="hero__cta btn">
<?= $page->hero_cta_text() ?>
</a>
<?php endif ?>
</div>
<?php if ($hero = $page->hero_image()->toFile()): ?>
<div class="hero__image">
<img src="<?= $hero->url() ?>" alt="<?= $page->hero_title() ?>">
</div>
<?php endif ?>
<!-- Floating Bubbles -->
<?php if ($page->floating_bubbles()->isNotEmpty()): ?>
<div class="hero__bubbles">
<?php foreach ($page->floating_bubbles()->toStructure() as $bubble): ?>
<div class="bubble bubble--<?= $bubble->position() ?>">
<?= $bubble->text() ?>
</div>
<?php endforeach ?>
</div>
<?php endif ?>
</section>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,33 @@
<?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);

View file

@ -1,57 +1,2 @@
<?php snippet('header') ?>
<main class="jouer">
<!-- Intro Section -->
<section class="jouer__intro">
<?php if ($page->intro_title()->isNotEmpty()): ?>
<h1 class="jouer__title"><?= $page->intro_title() ?></h1>
<?php endif ?>
<?php if ($page->intro_text()->isNotEmpty()): ?>
<p class="jouer__text"><?= $page->intro_text() ?></p>
<?php endif ?>
</section>
<!-- Games List -->
<section class="jouer__games">
<?php foreach ($page->children()->listed() as $game): ?>
<article class="game-card">
<!-- Badge -->
<?php if ($game->badge()->isNotEmpty() && $game->badge()->value() !== 'none'): ?>
<span class="game-card__badge game-card__badge--<?= $game->badge() ?>">
<?= $game->badge()->value() === 'new' ? 'NEW' : ($game->badge()->value() === 'coming_soon' ? 'INCOMING' : $game->badge()) ?>
</span>
<?php endif ?>
<!-- Cover -->
<?php if ($cover = $game->cover()->toFile()): ?>
<div class="game-card__cover">
<img src="<?= $cover->url() ?>" alt="<?= $game->title() ?>">
</div>
<?php endif ?>
<!-- Info -->
<div class="game-card__info">
<h2 class="game-card__title"><?= $game->title() ?></h2>
<?php if ($game->description()->isNotEmpty()): ?>
<p class="game-card__description"><?= $game->description() ?></p>
<?php endif ?>
<!-- Play Button -->
<?php if ($game->game_status()->value() === 'available'): ?>
<a href="<?= $game->url() ?>" class="game-card__play btn">
Jouer
</a>
<?php else: ?>
<span class="game-card__status btn btn--disabled">
Bientôt disponible
</span>
<?php endif ?>
</div>
</article>
<?php endforeach ?>
</section>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,32 @@
<?php
$specificData = [
'intro' => [
'title' => $page->intro_title()->value(),
'text' => $page->intro_text()->value()
],
'projects' => $page->children()->listed()->map(function($project) {
return [
'title' => $project->title()->value(),
'slug' => $project->slug(),
'url' => $project->url(),
'tagline' => $project->tagline()->value(),
'description' => $project->description()->value(),
'cover' => $project->cover()->toFile()?->url(),
'cover_thumb' => $project->cover()->toFile()?->thumb(['width' => 100])->url(),
'gallery' => $project->files()->filterBy('template', 'image')->limit(5)->map(function($img) {
return $img->url();
})->values(),
'impact' => $project->impact()->split(','),
'category' => $project->category()->value(),
'platforms' => $project->platforms()->split(','),
'apple_link' => $project->apple_link()->value(),
'android_link' => $project->android_link()->value()
];
})->values()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,109 +1,2 @@
<?php snippet('header') ?>
<main class="portfolio">
<!-- Intro Section -->
<section class="portfolio__intro">
<?php if ($page->intro_title()->isNotEmpty()): ?>
<h1 class="portfolio__title"><?= $page->intro_title() ?></h1>
<?php endif ?>
<?php if ($page->intro_text()->isNotEmpty()): ?>
<p class="portfolio__text"><?= $page->intro_text() ?></p>
<?php endif ?>
</section>
<!-- Projects Grid -->
<section class="portfolio__projects">
<?php
$projects = $page->children()->listed();
$total = $projects->count();
$index = 0;
?>
<?php foreach ($projects as $project): ?>
<?php $index++ ?>
<article class="project-card" data-index="<?= $index ?>">
<!-- Project Gallery -->
<div class="project-card__gallery">
<?php if ($cover = $project->cover()->toFile()): ?>
<img src="<?= $cover->url() ?>" alt="<?= $project->title() ?>" class="project-card__cover">
<?php endif ?>
<?php foreach ($project->files()->filterBy('template', 'image')->limit(5) as $image): ?>
<img src="<?= $image->url() ?>" alt="" class="project-card__image">
<?php endforeach ?>
</div>
<!-- Project Info -->
<div class="project-card__info">
<h2 class="project-card__title"><?= $project->title() ?></h2>
<?php if ($project->tagline()->isNotEmpty()): ?>
<p class="project-card__tagline"><?= $project->tagline() ?></p>
<?php endif ?>
<?php if ($project->description()->isNotEmpty()): ?>
<p class="project-card__description"><?= $project->description() ?></p>
<?php endif ?>
<!-- Meta -->
<div class="project-card__meta">
<?php if ($project->impact()->isNotEmpty()): ?>
<div class="project-card__impact">
<span class="label">Impact:</span>
<?= $project->impact()->join(', ') ?>
</div>
<?php endif ?>
<?php if ($project->category()->isNotEmpty()): ?>
<div class="project-card__category">
<span class="label">Catégorie:</span>
<?= $project->category()->value() ?>
</div>
<?php endif ?>
<?php if ($project->platforms()->isNotEmpty()): ?>
<div class="project-card__platforms">
<span class="label">Plateformes:</span>
<?= $project->platforms()->join(' / ') ?>
</div>
<?php endif ?>
</div>
<!-- Links -->
<div class="project-card__links">
<?php if ($project->apple_link()->isNotEmpty()): ?>
<a href="<?= $project->apple_link() ?>" class="btn btn--apple" target="_blank">
Apple
</a>
<?php endif ?>
<?php if ($project->android_link()->isNotEmpty()): ?>
<a href="<?= $project->android_link() ?>" class="btn btn--android" target="_blank">
Android
</a>
<?php endif ?>
</div>
</div>
<!-- Counter -->
<div class="project-card__counter">
<?= str_pad($index, 2, '0', STR_PAD_LEFT) ?>/<?= str_pad($total, 2, '0', STR_PAD_LEFT) ?>
</div>
</article>
<?php endforeach ?>
</section>
<!-- Thumbnails Navigation -->
<nav class="portfolio__thumbnails">
<?php foreach ($projects as $project): ?>
<a href="#<?= $project->slug() ?>" class="portfolio__thumbnail">
<?php if ($cover = $project->cover()->toFile()): ?>
<img src="<?= $cover->thumb(['width' => 100])->url() ?>" alt="<?= $project->title() ?>">
<?php endif ?>
</a>
<?php endforeach ?>
</nav>
</main>
<?php snippet('footer') ?>

View file

@ -0,0 +1,31 @@
<?php
$specificData = [
'tagline' => $page->tagline()->value(),
'description' => $page->description()->kt(),
'cover' => $page->cover()->toFile()?->url(),
'gallery' => $page->files()->filterBy('template', 'image')->map(function($img) {
return $img->url();
})->values(),
'impact' => $page->impact()->split(','),
'category' => $page->category()->value(),
'platforms' => $page->platforms()->split(','),
'client_name' => $page->client_name()->value(),
'apple_link' => $page->apple_link()->value(),
'android_link' => $page->android_link()->value(),
'web_link' => $page->web_link()->value(),
'prev' => $page->prev() ? [
'title' => $page->prev()->title()->value(),
'url' => $page->prev()->url()
] : null,
'next' => $page->next() ? [
'title' => $page->next()->title()->value(),
'url' => $page->next()->url()
] : null,
'parent_url' => $page->parent()->url()
];
$pageData = array_merge($genericData, $specificData);
header('Content-Type: application/json');
echo json_encode($pageData);

View file

@ -1,104 +1,2 @@
<?php snippet('header') ?>
<main class="project">
<article class="project__content">
<!-- Gallery -->
<div class="project__gallery">
<?php if ($cover = $page->cover()->toFile()): ?>
<img src="<?= $cover->url() ?>" alt="<?= $page->title() ?>" class="project__cover">
<?php endif ?>
<?php foreach ($page->files()->filterBy('template', 'image') as $image): ?>
<img src="<?= $image->url() ?>" alt="" class="project__image">
<?php endforeach ?>
</div>
<!-- Info -->
<div class="project__info">
<h1 class="project__title"><?= $page->title() ?></h1>
<?php if ($page->tagline()->isNotEmpty()): ?>
<p class="project__tagline"><?= $page->tagline() ?></p>
<?php endif ?>
<?php if ($page->description()->isNotEmpty()): ?>
<div class="project__description">
<?= $page->description()->kt() ?>
</div>
<?php endif ?>
<!-- Meta -->
<div class="project__meta">
<?php if ($page->impact()->isNotEmpty()): ?>
<div class="project__impact">
<span class="label">Impact:</span>
<?= $page->impact()->join(', ') ?>
</div>
<?php endif ?>
<?php if ($page->category()->isNotEmpty()): ?>
<div class="project__category">
<span class="label">Catégorie:</span>
<?= $page->category()->value() ?>
</div>
<?php endif ?>
<?php if ($page->platforms()->isNotEmpty()): ?>
<div class="project__platforms">
<span class="label">Plateformes:</span>
<?= $page->platforms()->join(' / ') ?>
</div>
<?php endif ?>
<?php if ($page->client_name()->isNotEmpty()): ?>
<div class="project__client">
<span class="label">Client:</span>
<?= $page->client_name() ?>
</div>
<?php endif ?>
</div>
<!-- Links -->
<div class="project__links">
<?php if ($page->apple_link()->isNotEmpty()): ?>
<a href="<?= $page->apple_link() ?>" class="btn btn--apple" target="_blank">
App Store
</a>
<?php endif ?>
<?php if ($page->android_link()->isNotEmpty()): ?>
<a href="<?= $page->android_link() ?>" class="btn btn--android" target="_blank">
Play Store
</a>
<?php endif ?>
<?php if ($page->web_link()->isNotEmpty()): ?>
<a href="<?= $page->web_link() ?>" class="btn btn--web" target="_blank">
Voir le site
</a>
<?php endif ?>
</div>
</div>
</article>
<!-- Navigation -->
<nav class="project__nav">
<?php if ($prev = $page->prev()): ?>
<a href="<?= $prev->url() ?>" class="project__nav-prev">
&larr; <?= $prev->title() ?>
</a>
<?php endif ?>
<a href="<?= $page->parent()->url() ?>" class="project__nav-back">
Tous les projets
</a>
<?php if ($next = $page->next()): ?>
<a href="<?= $next->url() ?>" class="project__nav-next">
<?= $next->title() ?> &rarr;
</a>
<?php endif ?>
</nav>
</main>
<?php snippet('footer') ?>

86
src/App.svelte Normal file
View file

@ -0,0 +1,86 @@
<script>
import { page } from '@state/page.svelte'
import Header from '@components/layout/Header.svelte'
import Footer from '@components/layout/Footer.svelte'
import Cursor from '@components/layout/Cursor.svelte'
import Home from '@views/Home.svelte'
import About from '@views/About.svelte'
import Expertise from '@views/Expertise.svelte'
import Portfolio from '@views/Portfolio.svelte'
import Project from '@views/Project.svelte'
import Jouer from '@views/Jouer.svelte'
import Game from '@views/Game.svelte'
import Blog from '@views/Blog.svelte'
import Article from '@views/Article.svelte'
import Default from '@views/Default.svelte'
const templates = {
'home': Home,
'about': About,
'expertise': Expertise,
'portfolio': Portfolio,
'project': Project,
'jouer': Jouer,
'game': Game,
'blog': Blog,
'article': Article,
'default': Default
}
const template = $derived(page.template || 'default')
const view = $derived(templates[template] || Default)
const pageData = $derived(page.data)
const showFooter = $derived(template !== 'home')
</script>
<div class="app">
<Cursor />
<Header />
<main class="main">
{#if pageData && view}
<view data={pageData} />
{/if}
</main>
{#if showFooter}
<Footer />
{/if}
</div>
<style>
:global(*) {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #000;
color: #fff;
overflow-x: hidden;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
:global(a) {
color: inherit;
text-decoration: none;
}
:global(img) {
max-width: 100%;
height: auto;
}
</style>

View file

@ -0,0 +1,68 @@
<script>
import { onMount } from 'svelte'
let cursorX = 0
let cursorY = 0
let outlineX = 0
let outlineY = 0
let rafId
onMount(() => {
const handleMouseMove = (e) => {
cursorX = e.clientX
cursorY = e.clientY
}
const animate = () => {
outlineX += (cursorX - outlineX) * 0.2
outlineY += (cursorY - outlineY) * 0.2
rafId = requestAnimationFrame(animate)
}
window.addEventListener('mousemove', handleMouseMove)
animate()
return () => {
window.removeEventListener('mousemove', handleMouseMove)
if (rafId) cancelAnimationFrame(rafId)
}
})
</script>
<div class="cursor" style="transform: translate({cursorX}px, {cursorY}px)"></div>
<div class="cursor-outline" style="transform: translate({outlineX}px, {outlineY}px)"></div>
<style>
.cursor,
.cursor-outline {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 9999;
}
.cursor {
width: 8px;
height: 8px;
background: #04fea0;
border-radius: 50%;
transform-origin: center;
}
.cursor-outline {
width: 40px;
height: 40px;
border: 2px solid #04fea0;
border-radius: 50%;
transform-origin: center;
margin: -16px 0 0 -16px;
}
@media (max-width: 768px) {
.cursor,
.cursor-outline {
display: none;
}
}
</style>

View file

@ -0,0 +1,73 @@
<script>
import { site } from '@state/site.svelte'
const siteTitle = $derived(site.title || 'World Game')
const currentYear = $derived(new Date().getFullYear())
</script>
<footer class="footer">
<div class="footer__container">
<div class="footer__brand">
<p class="footer__title">{siteTitle}</p>
<p class="footer__tagline">Play to Engage</p>
</div>
<div class="footer__copyright">
<p>&copy; {currentYear} {siteTitle}. Tous droits réservés.</p>
</div>
</div>
</footer>
<style>
.footer {
background: #000;
color: #fff;
padding: 3rem 2rem 2rem;
margin-top: auto;
}
.footer__container {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.footer__brand {
flex: 1;
}
.footer__title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 0.5rem;
}
.footer__tagline {
color: #04fea0;
margin: 0;
font-size: 0.9rem;
}
.footer__copyright {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.6);
}
.footer__copyright p {
margin: 0;
}
@media (max-width: 768px) {
.footer__container {
flex-direction: column;
text-align: center;
}
.footer__copyright {
order: 2;
}
}
</style>

View file

@ -0,0 +1,280 @@
<script>
import { navigation } from '@state/navigation.svelte'
import { locale } from '@state/locale.svelte'
import { page } from '@state/page.svelte'
import { navigateTo } from '@state/router'
const isMenuOpen = $derived(navigation.isMenuOpen)
const currentLang = $derived(locale.current)
const currentPage = $derived(page.template || 'home')
const translations = {
expertise: { fr: 'EXPERTISE', en: 'EXPERTISE' },
games: { fr: 'GAMES', en: 'GAMES' },
play: { fr: 'PLAY', en: 'PLAY' },
about: { fr: 'À PROPOS', en: 'ABOUT' },
blog: { fr: 'BLOG', en: 'BLOG' }
}
function t(key) {
return translations[key]?.[currentLang] || translations[key]?.fr || key
}
function handleNav(path) {
navigateTo(path)
}
function toggleMenu() {
navigation.toggleMenu()
}
</script>
<div class="navbar">
<div
class="clickable"
style="
z-index: 60;
position: absolute;
float: left;
left: 5vh;
top: 50%;
transform: translateY(-50%);
"
onclick={() => handleNav('/')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/')}
role="button"
tabindex="0"
>
<img
src="/assets/img/GIF_world_game_planete.gif"
alt="World Game"
class="clickable wg-logo"
/>
</div>
<div
class="navbar-item clickable"
class:active={currentPage === 'expertise'}
style="visibility: {isMenuOpen ? 'hidden' : 'visible'};"
onclick={() => handleNav('/expertise')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/expertise')}
role="button"
tabindex="0"
>
{t('expertise')}
</div>
<div
class="navbar-item clickable"
class:active={currentPage === 'portfolio'}
style="visibility: {isMenuOpen ? 'hidden' : 'visible'};"
onclick={() => handleNav('/portfolio')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/portfolio')}
role="button"
tabindex="0"
>
{t('games')}
</div>
<div
class="navbar-item clickable"
class:active={currentPage === 'jouer'}
style="visibility: {isMenuOpen ? 'hidden' : 'visible'};"
onclick={() => handleNav('/jouer')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/jouer')}
role="button"
tabindex="0"
>
{t('play')}
</div>
<div
class="navbar-item clickable"
class:active={currentPage === 'about'}
style="visibility: {isMenuOpen ? 'hidden' : 'visible'};"
onclick={() => handleNav('/a-propos')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/a-propos')}
role="button"
tabindex="0"
>
{t('about')}
</div>
<div
class="navbar-item clickable"
class:active={currentPage === 'blog' || currentPage === 'article'}
style="visibility: {isMenuOpen ? 'hidden' : 'visible'};"
onclick={() => handleNav('/blog')}
onkeypress={(e) => e.key === 'Enter' && handleNav('/blog')}
role="button"
tabindex="0"
>
{t('blog')}
</div>
<div
class="clickable"
style="
z-index: 60;
position: absolute;
float: right;
right: 5vh;
top: 50%;
transform: translateY(-50%);
"
onclick={toggleMenu}
onkeypress={(e) => e.key === 'Enter' && toggleMenu()}
role="button"
tabindex="0"
>
<svg
width="50"
height="50"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="height: 4.5vh; min-height: 36px;"
>
<circle
cx="7"
cy="7"
r="2"
fill="white"
style="
transform: {isMenuOpen ? 'translate(5px, 5px) scale(0.7)' : 'translate(0px, 0px) scale(1)'};
transform-origin: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
"
/>
<circle
cx="17"
cy="7"
r="2"
fill="white"
style="
transform: {isMenuOpen ? 'translate(-5px, 5px) scale(0.7)' : 'translate(0px, 0px) scale(1)'};
transform-origin: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
"
/>
<circle
cx="7"
cy="17"
r="2"
fill="white"
style="
transform: {isMenuOpen ? 'translate(5px, -5px) scale(0.7)' : 'translate(0px, 0px) scale(1)'};
transform-origin: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
"
/>
<circle
cx="17"
cy="17"
r="2"
fill="white"
style="
transform: {isMenuOpen ? 'translate(-5px, -5px) scale(0.7)' : 'translate(0px, 0px) scale(1)'};
transform-origin: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
"
/>
<line
x1="9"
y1="9"
x2="15"
y2="15"
stroke="white"
stroke-width="2.5"
stroke-linecap="round"
style="
opacity: {isMenuOpen ? 1 : 0};
transition: opacity 0.2s ease-in-out;
transition-delay: {isMenuOpen ? '0.1s' : '0s'};
"
/>
<line
x1="15"
y1="9"
x2="9"
y2="15"
stroke="white"
stroke-width="2.5"
stroke-linecap="round"
style="
opacity: {isMenuOpen ? 1 : 0};
transition: opacity 0.2s ease-in-out;
transition-delay: {isMenuOpen ? '0.1s' : '0s'};
"
/>
</svg>
</div>
</div>
<style>
.navbar {
width: 100%;
align-items: center;
justify-self: center;
position: fixed;
top: 0;
left: 0;
z-index: 50;
font-family: "Danzza";
font-size: var(--font-size-paragraph);
font-weight: normal;
color: white;
text-align: center !important;
text-transform: uppercase;
padding: 2vh 0 2vh 0;
backdrop-filter: blur(0px);
transition: all 0.3s ease-in-out;
}
.navbar-item {
display: inline-block;
padding: 1vmax 2vmax;
font-weight: bold;
vertical-align: middle;
position: relative;
z-index: 5;
color: white;
transition: color 0.3s;
}
.navbar-item.active {
color: #04fea0;
}
.navbar-item:hover {
color: #04fea0;
}
.wg-logo {
height: 4.8vh;
pointer-events: none;
}
.clickable {
cursor: pointer;
user-select: none;
}
@media screen and (max-width: 700px) {
.navbar-item {
font-size: var(--font-size-paragraph-mobile);
padding: 1vmax 1.5vmax;
}
}
@media screen and (min-width: 701px) and (max-width: 912px) {
.navbar-item {
font-size: var(--font-size-paragraph-tablet);
padding: 1vmax 1.8vmax;
}
}
</style>

View file

@ -0,0 +1,98 @@
<script>
let { variant = 'primary' // primary, secondary, outline } = $props()
let { size = 'medium' // small, medium, large } = $props()
let { disabled = false } = $props()
let { href = null } = $props()
</script>
{#if href}
<a
{href}
class="btn btn--{variant} btn--{size}"
class:disabled
on:click
>
<slot />
</a>
{:else}
<button
class="btn btn--{variant} btn--{size}"
{disabled}
on:click
>
<slot />
</button>
{/if}
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 2rem;
font-weight: 600;
text-decoration: none;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.9rem;
}
.btn--primary {
background: #04fea0;
color: #000;
border-color: #04fea0;
}
.btn--primary:hover {
background: #03d98c;
border-color: #03d98c;
}
.btn--secondary {
background: #000;
color: #04fea0;
border-color: #04fea0;
}
.btn--secondary:hover {
background: #04fea0;
color: #000;
}
.btn--outline {
background: transparent;
color: #fff;
border-color: #fff;
}
.btn--outline:hover {
background: #fff;
color: #000;
}
.btn--small {
padding: 0.5rem 1.5rem;
font-size: 0.8rem;
}
.btn--medium {
padding: 0.75rem 2rem;
font-size: 0.9rem;
}
.btn--large {
padding: 1rem 2.5rem;
font-size: 1rem;
}
.btn:disabled,
.btn.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
</style>

View file

@ -0,0 +1,68 @@
<script>
import { onMount } from 'svelte'
let { src } = $props()
let { poster = '' } = $props()
let { overlay = true } = $props()
let videoElement
onMount(() => {
if (videoElement && src) {
videoElement.play().catch(err => {
console.log('Autoplay failed:', err)
})
}
})
</script>
<div class="video-background">
{#if src}
<video
bind:this={videoElement}
{poster}
muted
loop
playsinline
preload="metadata"
class="video-background__video"
>
<source {src} type="video/mp4" />
</video>
{/if}
{#if overlay}
<div class="video-background__overlay"></div>
{/if}
</div>
<style>
.video-background {
position: absolute;
inset: 0;
overflow: hidden;
z-index: 0;
}
.video-background__video {
position: absolute;
top: 50%;
left: 50%;
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
transform: translate(-50%, -50%);
object-fit: cover;
}
.video-background__overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3),
rgba(0, 0, 0, 0.7)
);
}
</style>

12
src/main.js Normal file
View file

@ -0,0 +1,12 @@
import './styles/index.css'
import App from './App.svelte'
import { mount } from 'svelte'
import { initRouter } from './state/router'
initRouter()
const app = mount(App, {
target: document.getElementById('app')
})
export default app

142
src/state/animations.js Normal file
View file

@ -0,0 +1,142 @@
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
/**
* Fade in animation for page enter
*/
export function pageEnter(element, options = {}) {
const defaults = {
opacity: 0,
y: 50,
duration: 0.8,
ease: 'power3.out'
}
return gsap.from(element, { ...defaults, ...options })
}
/**
* Fade out animation for page exit
*/
export function pageExit(element, options = {}) {
const defaults = {
opacity: 0,
y: -50,
duration: 0.5,
ease: 'power3.in'
}
return gsap.to(element, { ...defaults, ...options })
}
/**
* Carousel slide animation
*/
export function carouselSlide(element, offset, options = {}) {
const defaults = {
x: `-${offset}%`,
duration: 0.8,
ease: 'power2.inOut'
}
return gsap.to(element, { ...defaults, ...options })
}
/**
* Scroll reveal animation
*/
export function scrollReveal(elements, options = {}) {
const defaults = {
scrollTrigger: {
trigger: elements,
start: 'top 80%',
toggleActions: 'play none none none'
},
y: 50,
opacity: 0,
stagger: 0.1,
duration: 0.8,
ease: 'power3.out'
}
return gsap.from(elements, { ...defaults, ...options })
}
/**
* Fade in elements on scroll
*/
export function fadeInOnScroll(elements, options = {}) {
const defaults = {
scrollTrigger: {
trigger: elements,
start: 'top 80%'
},
opacity: 0,
duration: 1,
stagger: 0.2,
ease: 'power2.out'
}
return gsap.from(elements, { ...defaults, ...options })
}
/**
* Scale animation
*/
export function scaleIn(element, options = {}) {
const defaults = {
scale: 0,
duration: 0.5,
ease: 'back.out(1.7)'
}
return gsap.from(element, { ...defaults, ...options })
}
/**
* Cleanup all ScrollTrigger instances
*/
export function cleanupScrollTriggers() {
ScrollTrigger.getAll().forEach(trigger => trigger.kill())
}
/**
* Refresh ScrollTrigger (useful after content changes)
*/
export function refreshScrollTriggers() {
ScrollTrigger.refresh()
}
/**
* Custom cursor follow animation
*/
export function cursorFollow(cursorElement, outlineElement) {
let cursorX = 0
let cursorY = 0
let outlineX = 0
let outlineY = 0
const handleMouseMove = (e) => {
cursorX = e.clientX
cursorY = e.clientY
}
const animate = () => {
outlineX += (cursorX - outlineX) * 0.2
outlineY += (cursorY - outlineY) * 0.2
gsap.set(cursorElement, { x: cursorX, y: cursorY })
gsap.set(outlineElement, { x: outlineX, y: outlineY })
requestAnimationFrame(animate)
}
window.addEventListener('mousemove', handleMouseMove)
animate()
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}

View file

@ -0,0 +1,13 @@
let current = $state('fr')
let languages = $state([])
export const locale = {
get current() { return current },
get languages() { return languages },
setLanguage: (code) => current = code,
initialize: (language, langs) => {
current = language
languages = langs
}
}

View file

@ -0,0 +1,12 @@
let isMenuOpen = $state(false)
let isLoading = $state(false)
export const navigation = {
get isMenuOpen() { return isMenuOpen },
get isLoading() { return isLoading },
toggleMenu: () => isMenuOpen = !isMenuOpen,
openMenu: () => isMenuOpen = true,
closeMenu: () => isMenuOpen = false,
setLoading: (value) => isLoading = value
}

35
src/state/page.svelte.js Normal file
View file

@ -0,0 +1,35 @@
let data = $state(null)
let template = $state(null)
let url = $state(null)
let loading = $state(false)
let error = $state(null)
export const page = {
get data() { return data },
get template() { return template },
get url() { return url },
get loading() { return loading },
get error() { return error },
set: (pageData) => {
data = pageData.data
template = pageData.template
url = pageData.url
loading = pageData.loading ?? false
error = pageData.error ?? null
},
setLoading: (value) => loading = value,
setError: (err) => {
error = err
loading = false
},
reset: () => {
data = null
template = null
url = null
loading = false
error = null
}
}

96
src/state/router.js Normal file
View file

@ -0,0 +1,96 @@
import navaid from "navaid";
import { page } from "./page.svelte";
import { navigation } from "./navigation.svelte";
import { site } from "./site.svelte";
import { locale } from "./locale.svelte";
export const router = navaid("/", () => {
// Default handler
});
async function loadPage(path) {
navigation.setLoading(true);
page.setLoading(true);
try {
const response = await fetch(`${path}.json`);
if (!response.ok) {
throw new Error(`Failed to load page: ${response.status}`);
}
const data = await response.json();
if (data.site) {
site.set(data.site);
locale.initialize(data.site.language, data.site.languages);
}
page.set({
data,
template: data.template || "default",
url: path,
loading: false,
error: null,
});
window.scrollTo(0, 0);
} catch (error) {
console.error("Failed to load page:", error);
page.setError(error);
} finally {
navigation.setLoading(false);
}
}
// Route handlers
router
.on("/", () => loadPage("/home"))
.on("/expertise", () => loadPage("/expertise"))
.on("/portfolio", () => loadPage("/portfolio"))
.on("/portfolio/:slug", ({ slug }) => loadPage(`/portfolio/${slug}`))
.on("/jouer", () => loadPage("/jouer"))
.on("/jouer/:slug", ({ slug }) => loadPage(`/jouer/${slug}`))
.on("/a-propos", () => loadPage("/a-propos"))
.on("/blog", () => loadPage("/blog"))
.on("/blog/:slug", ({ slug }) => loadPage(`/blog/${slug}`))
.on("*", (params) => {
// Fallback for other routes
loadPage(window.location.pathname);
});
export function initRouter() {
// Load initial page data
loadPage(window.location.pathname);
// Start listening to route changes
router.listen();
// Intercept internal link clicks
document.addEventListener("click", (e) => {
const link = e.target.closest("a");
if (!link) return;
const url = new URL(link.href, window.location.origin);
// Only intercept same-origin links without target attribute
if (
url.origin === window.location.origin &&
!link.target &&
!link.hasAttribute("download")
) {
e.preventDefault();
navigateTo(url.pathname);
}
});
// Handle browser back/forward
window.addEventListener("popstate", () => {
loadPage(window.location.pathname);
});
}
export function navigateTo(path) {
window.history.pushState({}, "", path);
loadPage(path);
}

24
src/state/site.svelte.js Normal file
View file

@ -0,0 +1,24 @@
let title = $state('')
let url = $state('')
let language = $state('fr')
let languages = $state([])
let logo = $state(null)
let navigation = $state([])
export const site = {
get title() { return title },
get url() { return url },
get language() { return language },
get languages() { return languages },
get logo() { return logo },
get navigation() { return navigation },
set: (data) => {
title = data.title || ''
url = data.url || ''
language = data.language || 'fr'
languages = data.languages || []
logo = data.logo || null
navigation = data.navigation || []
}
}

289
src/style.css Normal file
View file

@ -0,0 +1,289 @@
/* FONT SIZING SYSTEM - Consistent typography across the site */
:root {
/* Base font sizes for desktop */
--font-size-paragraph: 18px;
--font-size-paragraph-small: 16px;
--font-size-subtitle: 20px;
--font-size-title-section: 32px;
--font-size-title-main: 48px;
--font-size-title-hero: 96px;
--font-size-button: 13px;
--font-size-caption: 12px;
/* Mobile font sizes */
--font-size-paragraph-mobile: 16px;
--font-size-paragraph-small-mobile: 12px;
--font-size-subtitle-mobile: 16px;
--font-size-title-section-mobile: 24px;
--font-size-title-main-mobile: 32px;
--font-size-title-hero-mobile: 48px;
--font-size-button-mobile: 11px;
--font-size-caption-mobile: 10px;
/* Tablet font sizes */
--font-size-paragraph-tablet: 16px;
--font-size-paragraph-small-tablet: 14px;
--font-size-subtitle-tablet: 18px;
--font-size-title-section-tablet: 28px;
--font-size-title-main-tablet: 40px;
--font-size-title-hero-tablet: 64px;
--font-size-button-tablet: 12px;
--font-size-caption-tablet: 11px;
}
html,
body {
height: 100%;
min-height: 100vh;
min-height: -webkit-fill-available;
min-height: calc(var(--vh, 1vh) * 100);
user-select: none;
}
body {
margin: 0;
font-family: "Danzza Regular", "Danzza", -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: none;
background: #000;
overflow-x: hidden;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
cursor: none;
}
/* Font faces */
@font-face {
font-family: "Terminal";
font-weight: bold;
src: local("terminal-grotesque"),
url("/assets/fonts/terminal-grotesque.ttf") format("truetype");
}
@font-face {
font-family: "Danzza";
src: local("Danzza Regular"),
url("/assets/fonts/Danzza-Regular.woff") format("woff"),
url("/assets/fonts/Danzza-Regular.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Light";
src: local("Danzza Light"),
url("/assets/fonts/Danzza-Light.woff") format("woff"),
url("/assets/fonts/Danzza-Light.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Medium";
src: local("Danzza Medium"),
url("/assets/fonts/Danzza-Medium.woff") format("woff"),
url("/assets/fonts/Danzza-Medium.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Bold";
src: local("Danzza Bold"),
url("/assets/fonts/Danzza-Bold.woff") format("woff"),
url("/assets/fonts/Danzza-Bold.otf") format("opentype");
font-display: swap;
}
.font-face-terminal {
font-family: "Terminal";
}
.font-face-danzza {
font-family: "Danzza";
}
.font-face-danzza-light {
font-family: "Danzza Light";
}
.font-face-danzza-medium {
font-family: "Danzza Medium";
}
.font-face-danzza-bold {
font-family: "Danzza Bold";
}
/* Golden Grid */
.golden-grid {
height: 100% !important;
min-height: 100% !important;
display: grid !important;
position: relative;
grid-template-rows: 1fr 1fr 2fr 4fr 2.66fr 5.33fr 5.33fr 4.33fr 2.83fr 3.5fr 3.5fr 2.83fr 4.33fr 5.33fr 5.33fr 2.66fr 4fr 2fr 1fr 1fr;
grid-template-columns: 1fr 1fr 2fr 4fr 2.66fr 5.33fr 5.33fr 4.33fr 2.83fr 3.5fr 3.5fr 2.83fr 4.33fr 5.33fr 5.33fr 2.66fr 4fr 2fr 1fr 1fr;
text-align: center;
}
.slide {
overflow-y: hidden;
}
/* Vertical Lines */
.vertical-line {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
height: 150%;
}
.vertical-line-start {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/6 / span 20 / span 1;
height: 150%;
}
.vertical-line-center {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/11 / span 20 / span 1;
height: 150%;
}
.vertical-line-end {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/16 / span 20 / span 1;
height: 150%;
}
/* Button */
.button {
width: 14vmax;
min-width: 130px;
display: flex;
align-items: center;
justify-content: center;
position: sticky;
z-index: 1;
padding: 12px 16px;
transition: 0.5s ease-out;
font-family: "Danzza Bold";
background-color: #04fea0;
border: none;
cursor: pointer;
}
.button:hover {
background-position: left;
background-color: transparent;
outline: solid 2px #04fea0;
}
.button p {
color: black;
margin: 0;
transition: color 0.3s;
}
.button:hover p {
color: #04fea0 !important;
}
.earth-icon {
width: 24px;
height: 24px;
background-image: url('/assets/img/icon-earth-green.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-right: 8px;
transition: filter 0.3s;
}
.button:hover .earth-icon {
filter: brightness(0) saturate(100%) invert(77%) sepia(82%) saturate(507%) hue-rotate(91deg) brightness(101%) contrast(97%);
}
/* Clickable elements */
.clickable {
cursor: pointer;
user-select: none;
}
/* Cursor */
#cursor-dot,
#cursor-dot-outline,
#cursor-circle {
position: absolute;
top: 50%;
left: 50%;
z-index: 99999;
transform: translate(-50%, -50%);
transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
border-radius: 50%;
pointer-events: none;
opacity: 0;
}
#cursor-dot {
width: 14px;
height: 14px;
background-color: white;
}
#cursor-circle {
width: 50px;
height: 50px;
border-width: 3px;
border-style: solid;
border-color: #04fea0;
}
#cursor-dot-outline {
width: 13px;
height: 13px;
background-color: white;
}
@media (pointer: coarse) {
#cursor-dot,
#cursor-dot-outline,
#cursor-circle {
display: none;
}
body,
* {
cursor: auto;
}
}
/* Selection */
::selection {
background: #04fea0;
color: #000;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #000;
}
::-webkit-scrollbar-thumb {
background: #04fea0;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #03d98c;
}

53
src/styles/buttons.css Normal file
View file

@ -0,0 +1,53 @@
/* Button */
.button {
width: 14vmax;
min-width: 130px;
display: flex;
align-items: center;
justify-content: center;
position: sticky;
z-index: 1;
padding: 12px 16px;
transition: 0.5s ease-out;
font-family: "Danzza Bold";
background-color: var(--color-primary);
border: none;
cursor: pointer;
}
.button:hover {
background-position: left;
background-color: transparent;
outline: solid 2px var(--color-primary);
}
.button p {
color: black;
margin: 0;
transition: color 0.3s;
}
.button:hover p {
color: var(--color-primary) !important;
}
.earth-icon {
width: 24px;
height: 24px;
background-image: url('/assets/img/icon-earth-green.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-right: 8px;
transition: filter 0.3s;
}
.button:hover .earth-icon {
filter: brightness(0) saturate(100%) invert(77%) sepia(82%) saturate(507%) hue-rotate(91deg) brightness(101%) contrast(97%);
}
/* Clickable elements */
.clickable {
cursor: pointer;
user-select: none;
}

42
src/styles/cursor.css Normal file
View file

@ -0,0 +1,42 @@
/* Custom Cursor */
#cursor-dot,
#cursor-dot-outline,
#cursor-circle {
position: absolute;
top: 50%;
left: 50%;
z-index: 99999;
transform: translate(-50%, -50%);
transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
border-radius: 50%;
pointer-events: none;
opacity: 0;
}
#cursor-dot {
width: 14px;
height: 14px;
background-color: white;
}
#cursor-circle {
width: 50px;
height: 50px;
border-width: 3px;
border-style: solid;
border-color: var(--color-primary);
}
#cursor-dot-outline {
width: 13px;
height: 13px;
background-color: white;
}
@media (pointer: coarse) {
#cursor-dot,
#cursor-dot-outline,
#cursor-circle {
display: none;
}
}

60
src/styles/fonts.css Normal file
View file

@ -0,0 +1,60 @@
/* Font faces */
@font-face {
font-family: "Terminal";
font-weight: bold;
src: local("terminal-grotesque"),
url("/assets/fonts/terminal-grotesque.ttf") format("truetype");
}
@font-face {
font-family: "Danzza";
src: local("Danzza Regular"),
url("/assets/fonts/Danzza-Regular.woff") format("woff"),
url("/assets/fonts/Danzza-Regular.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Light";
src: local("Danzza Light"),
url("/assets/fonts/Danzza-Light.woff") format("woff"),
url("/assets/fonts/Danzza-Light.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Medium";
src: local("Danzza Medium"),
url("/assets/fonts/Danzza-Medium.woff") format("woff"),
url("/assets/fonts/Danzza-Medium.otf") format("opentype");
font-display: swap;
}
@font-face {
font-family: "Danzza Bold";
src: local("Danzza Bold"),
url("/assets/fonts/Danzza-Bold.woff") format("woff"),
url("/assets/fonts/Danzza-Bold.otf") format("opentype");
font-display: swap;
}
/* Font utility classes */
.font-face-terminal {
font-family: "Terminal";
}
.font-face-danzza {
font-family: "Danzza";
}
.font-face-danzza-light {
font-family: "Danzza Light";
}
.font-face-danzza-medium {
font-family: "Danzza Medium";
}
.font-face-danzza-bold {
font-family: "Danzza Bold";
}

8
src/styles/index.css Normal file
View file

@ -0,0 +1,8 @@
/* Main styles entry point */
@import './variables.css';
@import './reset.css';
@import './fonts.css';
@import './layout.css';
@import './buttons.css';
@import './cursor.css';
@import './utils.css';

42
src/styles/layout.css Normal file
View file

@ -0,0 +1,42 @@
/* Golden Grid */
.golden-grid {
height: 100% !important;
min-height: 100% !important;
display: grid !important;
position: relative;
grid-template-rows: 1fr 1fr 2fr 4fr 2.66fr 5.33fr 5.33fr 4.33fr 2.83fr 3.5fr 3.5fr 2.83fr 4.33fr 5.33fr 5.33fr 2.66fr 4fr 2fr 1fr 1fr;
grid-template-columns: 1fr 1fr 2fr 4fr 2.66fr 5.33fr 5.33fr 4.33fr 2.83fr 3.5fr 3.5fr 2.83fr 4.33fr 5.33fr 5.33fr 2.66fr 4fr 2fr 1fr 1fr;
text-align: center;
}
.slide {
overflow-y: hidden;
}
/* Vertical Lines */
.vertical-line {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
height: 150%;
}
.vertical-line-start {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/6 / span 20 / span 1;
height: 150%;
}
.vertical-line-center {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/11 / span 20 / span 1;
height: 150%;
}
.vertical-line-end {
z-index: 1;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
grid-area: 1/16 / span 20 / span 1;
height: 150%;
}

34
src/styles/reset.css Normal file
View file

@ -0,0 +1,34 @@
/* CSS Reset */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
cursor: none;
}
html,
body {
height: 100%;
min-height: 100vh;
min-height: -webkit-fill-available;
min-height: calc(var(--vh, 1vh) * 100);
user-select: none;
}
body {
margin: 0;
font-family: "Danzza Regular", "Danzza", -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--color-background);
overflow-x: hidden;
}
@media (pointer: coarse) {
body,
* {
cursor: auto;
}
}

23
src/styles/utils.css Normal file
View file

@ -0,0 +1,23 @@
/* Selection */
::selection {
background: var(--color-primary);
color: #000;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #000;
}
::-webkit-scrollbar-thumb {
background: var(--color-primary);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-primary-hover);
}

38
src/styles/variables.css Normal file
View file

@ -0,0 +1,38 @@
/* CSS Variables */
:root {
/* Base font sizes for desktop */
--font-size-paragraph: 18px;
--font-size-paragraph-small: 16px;
--font-size-subtitle: 20px;
--font-size-title-section: 32px;
--font-size-title-main: 48px;
--font-size-title-hero: 96px;
--font-size-button: 13px;
--font-size-caption: 12px;
/* Mobile font sizes */
--font-size-paragraph-mobile: 16px;
--font-size-paragraph-small-mobile: 12px;
--font-size-subtitle-mobile: 16px;
--font-size-title-section-mobile: 24px;
--font-size-title-main-mobile: 32px;
--font-size-title-hero-mobile: 48px;
--font-size-button-mobile: 11px;
--font-size-caption-mobile: 10px;
/* Tablet font sizes */
--font-size-paragraph-tablet: 16px;
--font-size-paragraph-small-tablet: 14px;
--font-size-subtitle-tablet: 18px;
--font-size-title-section-tablet: 28px;
--font-size-title-main-tablet: 40px;
--font-size-title-hero-tablet: 64px;
--font-size-button-tablet: 12px;
--font-size-caption-tablet: 11px;
/* Colors */
--color-primary: #04fea0;
--color-primary-hover: #03d98c;
--color-background: #000;
--color-text: #fff;
}

177
src/views/About.svelte Normal file
View file

@ -0,0 +1,177 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
const intro = $derived(data?.intro || {})
const mission = $derived(data?.mission || {})
const manifesto = $derived(data?.manifesto || {})
const team = $derived(data?.team || {})
</script>
<div class="about" transition:fade>
<section class="about__intro">
<h1>{intro.title || data?.title}</h1>
{#if intro.text}
<p class="about__intro-text">{@html intro.text}</p>
{/if}
</section>
{#if mission.text}
<section class="about__section">
<h2>{mission.title}</h2>
<div class="about__section-content">
{@html mission.text}
</div>
</section>
{/if}
{#if manifesto.text}
<section class="about__section">
<h2>{manifesto.title}</h2>
<div class="about__section-content">
{@html manifesto.text}
</div>
</section>
{/if}
{#if team.members && team.members.length > 0}
<section class="about__team">
<h2>{team.title}</h2>
<div class="about__team-grid">
{#each team.members as member}
<article class="team-card">
{#if member.photo}
<div class="team-card__photo">
<img src={member.photo} alt={member.name} />
</div>
{/if}
<div class="team-card__info">
<h3>{member.name}</h3>
<p class="team-card__role">{member.role}</p>
{#if member.bio}
<p class="team-card__bio">{member.bio}</p>
{/if}
</div>
</article>
{/each}
</div>
</section>
{/if}
</div>
<style>
.about {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.about__intro {
max-width: 1200px;
margin: 0 auto 6rem;
text-align: center;
}
.about__intro h1 {
font-size: clamp(2.5rem, 6vw, 5rem);
margin-bottom: 2rem;
}
.about__intro-text {
font-size: clamp(1.1rem, 2vw, 1.5rem);
opacity: 0.9;
max-width: 900px;
margin: 0 auto;
}
.about__section {
max-width: 1200px;
margin: 4rem auto;
}
.about__section h2 {
font-size: clamp(2rem, 4vw, 3rem);
margin-bottom: 2rem;
color: #04fea0;
}
.about__section-content {
line-height: 1.8;
opacity: 0.9;
}
.about__team {
max-width: 1400px;
margin: 6rem auto 0;
}
.about__team h2 {
font-size: clamp(2rem, 4vw, 3rem);
margin-bottom: 3rem;
text-align: center;
color: #04fea0;
}
.about__team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.team-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2rem;
border-radius: 8px;
transition: transform 0.3s, border-color 0.3s;
}
.team-card:hover {
transform: translateY(-5px);
border-color: #04fea0;
}
.team-card__photo {
margin-bottom: 1.5rem;
}
.team-card__photo img {
width: 100%;
height: 250px;
object-fit: cover;
border-radius: 4px;
}
.team-card h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.team-card__role {
color: #04fea0;
font-size: 0.9rem;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.team-card__bio {
font-size: 0.95rem;
line-height: 1.6;
opacity: 0.8;
}
@media (max-width: 768px) {
.about {
padding: 6rem 1rem 3rem;
}
.about__team-grid {
grid-template-columns: 1fr;
}
}
</style>

24
src/views/Article.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="article" transition:fade>
<div class="article__container">
<h1>{data?.title || 'Article'}</h1>
<p>Article view - To be implemented</p>
</div>
</div>
<style>
.article {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.article__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

24
src/views/Blog.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="blog" transition:fade>
<div class="blog__container">
<h1>{data?.title || 'Blog'}</h1>
<p>Blog view - To be implemented</p>
</div>
</div>
<style>
.blog {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.blog__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

45
src/views/Default.svelte Normal file
View file

@ -0,0 +1,45 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="default" transition:fade>
<div class="default__container">
<h1>{data?.title || 'Page'}</h1>
{#if data?.body}
<div class="default__content">
{@html data.body}
</div>
{/if}
</div>
</div>
<style>
.default {
min-height: 100vh;
padding: 8rem 2rem 4rem;
}
.default__container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
font-size: clamp(2rem, 5vw, 4rem);
margin-bottom: 2rem;
color: #fff;
}
.default__content {
color: rgba(255, 255, 255, 0.9);
line-height: 1.8;
}
@media (max-width: 768px) {
.default {
padding: 6rem 1rem 3rem;
}
}
</style>

View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="expertise" transition:fade>
<div class="expertise__container">
<h1>{data?.title || 'Expertise'}</h1>
<p>Expertise view - To be implemented</p>
</div>
</div>
<style>
.expertise {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.expertise__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

24
src/views/Game.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="game" transition:fade>
<div class="game__container">
<h1>{data?.title || 'Game'}</h1>
<p>Game view - To be implemented</p>
</div>
</div>
<style>
.game {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.game__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

218
src/views/Home.svelte Normal file
View file

@ -0,0 +1,218 @@
<script>
import { onMount } from 'svelte'
import { locale } from '@state/locale.svelte'
import { navigateTo } from '@state/router'
let { data } = $props()
const currentLang = $derived(locale.current)
const translations = {
homeText: {
fr: "World Game crée des expériences gamifiées qui transforment l'engagement en résultats mesurables",
en: "World Game creates gamified experiences that transform engagement into measurable results"
},
explore: {
fr: "EXPLORER",
en: "EXPLORE"
}
}
function t(key) {
return translations[key]?.[currentLang] || translations[key]?.fr || ''
}
onMount(() => {
const playVideo = async (videoId) => {
const video = document.getElementById(videoId)
if (video) {
try {
video.muted = true
video.playsInline = true
const playPromise = video.play()
if (playPromise !== undefined) {
await playPromise
}
} catch (error) {
console.log(`Autoplay failed for ${videoId}:`, error)
const playOnInteraction = () => {
video.play().catch(e => console.log('Fallback play failed:', e))
document.removeEventListener('click', playOnInteraction)
document.removeEventListener('touchstart', playOnInteraction)
}
document.addEventListener('click', playOnInteraction)
document.addEventListener('touchstart', playOnInteraction)
}
}
}
const timer = setTimeout(() => {
playVideo('home-video')
playVideo('home-video-mobile')
}, 100)
return () => clearTimeout(timer)
})
function handleExplore() {
navigateTo('/portfolio')
}
</script>
<div class="home golden-grid slide" data-anchor="HOME">
<div style="grid-area: 1/1/span 20/span 20; filter: saturate(140%);">
<div class="olly">
<figure
style="
height: -webkit-fill-available;
width: -webkit-fill-available;
"
>
<video
muted
autoplay
playsinline
loop
controls={false}
preload="metadata"
id="home-video"
class="home-video home-video-desktop"
style="
object-fit: cover;
min-height: 100%;
min-width: 100%;
height: -webkit-fill-available;
width: -webkit-fill-available;
"
>
<source
src="/assets/video/Website_version.mp4"
type="video/mp4"
/>
</video>
<video
muted
autoplay
playsinline
loop
controls={false}
preload="metadata"
id="home-video-mobile"
class="home-video-mobile"
style="
object-fit: cover;
min-height: 100%;
min-width: 100%;
height: -webkit-fill-available;
width: -webkit-fill-available;
"
>
<source
src="/assets/video/mobile_version_texte_fixe.mp4"
type="video/mp4"
/>
</video>
</figure>
</div>
</div>
<div class="vertical-line-start"></div>
<div class="vertical-line-center"></div>
<div class="vertical-line-end"></div>
<div
class="home-text"
style="z-index: 5; justify-self: center; margin-top: 6vmax;"
>
<h2 class="font-face-danzza-light home-subtitle">
{t('homeText')}
</h2>
<div
class="clickable button"
style="margin: auto; margin-top: 40px;"
onclick={handleExplore}
onkeypress={(e) => e.key === 'Enter' && handleExplore()}
role="button"
tabindex="0"
>
<div class="earth-icon clickable-filter-black"></div>
<p class="clickable" style="font-family: Terminal; font-size: 1.2em;">
{t('explore')}
</p>
</div>
</div>
</div>
<style>
.home {
background-color: rgba(0, 0, 0, 0);
}
.home-text {
z-index: 9;
grid-area: 9/1 / span 6 / span 20;
width: 100%;
}
.home-subtitle {
font-size: var(--font-size-subtitle);
color: white;
width: 30%;
margin: auto;
}
.olly {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.olly figure {
margin: 0;
padding: 0;
position: relative;
width: 100%;
height: 100%;
}
.home-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.home-video-desktop {
display: block !important;
}
.home-video-mobile {
display: none !important;
}
@media screen and (max-width: 1000px) {
.home-video-desktop {
display: none !important;
}
.home-video-mobile {
display: block !important;
}
.home-subtitle {
font-size: var(--font-size-subtitle-mobile);
width: 80%;
line-height: 1.4;
}
}
@media screen and (min-width: 701px) and (max-width: 912px) {
.home-subtitle {
font-size: var(--font-size-subtitle-tablet);
width: 70%;
}
}
</style>

24
src/views/Jouer.svelte Normal file
View file

@ -0,0 +1,24 @@
<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>

View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="portfolio" transition:fade>
<div class="portfolio__container">
<h1>{data?.title || 'Portfolio'}</h1>
<p>Portfolio view - To be implemented</p>
</div>
</div>
<style>
.portfolio {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.portfolio__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

24
src/views/Project.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { fade } from 'svelte/transition'
let { data } = $props()
</script>
<div class="project" transition:fade>
<div class="project__container">
<h1>{data?.title || 'Project'}</h1>
<p>Project view - To be implemented</p>
</div>
</div>
<style>
.project {
min-height: 100vh;
padding: 8rem 2rem 4rem;
color: #fff;
}
.project__container {
max-width: 1200px;
margin: 0 auto;
}
</style>

5
svelte.config.js Normal file
View file

@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
preprocess: vitePreprocess()
}

40
vite.config.js Normal file
View file

@ -0,0 +1,40 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import path from 'path'
export default defineConfig({
plugins: [
svelte({
configFile: './svelte.config.js'
})
],
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@views': path.resolve(__dirname, 'src/views'),
'@state': path.resolve(__dirname, 'src/state')
}
},
server: {
port: 5173,
proxy: {
'^(?!/@vite|/@fs|/node_modules|/src).*': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
},
build: {
outDir: 'assets/dist',
emptyOutDir: true,
manifest: false,
rollupOptions: {
input: 'src/main.js',
output: {
entryFileNames: 'index.js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]'
}
}
}
})