Compare commits
No commits in common. "0505cc7b8e1fe02d345ea12c908674abc97738e9" and "34635f798208052a4be353200e4bdc0489c05cb3" have entirely different histories.
0505cc7b8e
...
34635f7982
7 changed files with 78 additions and 671 deletions
|
|
@ -43,12 +43,17 @@ tabs:
|
|||
cover: true
|
||||
ratio: 16/9
|
||||
info: "{{ page.date.toDate('d/m/Y') }}"
|
||||
articles:
|
||||
articlesList:
|
||||
label: Articles
|
||||
type: pages
|
||||
headline: Liste des articles
|
||||
layout: cards
|
||||
sortBy: published desc
|
||||
size: full
|
||||
image:
|
||||
query: page.cover.toFile
|
||||
ratio: 16/9
|
||||
cover: true
|
||||
info: "{{ page.published.toDate('d/m/Y') }}"
|
||||
template: article
|
||||
info: "{{ page.date.toDate('d/m/Y') }}"
|
||||
create: article
|
||||
sortBy: date desc
|
||||
|
|
@ -6,19 +6,26 @@ if ($related->isEmpty()) {
|
|||
}
|
||||
|
||||
$specificData = [
|
||||
'date' => $page->date()->toDate('d/m/Y'),
|
||||
'intro' => $page->intro()->value(),
|
||||
'cover' => $page->cover()->toFile()?->url(),
|
||||
'body' => (string) $page->body()->toBlocks(),
|
||||
'related' => $related->map(function($rec) {
|
||||
'date' => $page->date()->toDate('Y-m-d'),
|
||||
'dateFormatted' => $page->date()->toDate('d/m/Y'),
|
||||
'intro' => $page->intro()->value(),
|
||||
'author' => [
|
||||
'name' => $page->authorName()->value(),
|
||||
'role' => $page->authorRole()->value(),
|
||||
'photo' => $page->authorPhoto()->toFile()?->url()
|
||||
],
|
||||
'cover' => $page->cover()->toFile()?->url(),
|
||||
'content' => $page->articleContent()->toBlocks(),
|
||||
'tags' => $page->tags()->split(),
|
||||
'related' => $related->map(function($rec) {
|
||||
return [
|
||||
'title' => $rec->title()->value(),
|
||||
'slug' => $rec->slug(),
|
||||
'date' => $rec->date()->toDate('d/m/Y'),
|
||||
'cover' => $rec->cover()->toFile()?->url(),
|
||||
'url' => $rec->url(),
|
||||
'category' => $rec->category()->value(),
|
||||
'cover' => $rec->cover()->toFile()?->thumb(['width' => 400])->url()
|
||||
];
|
||||
})->values(),
|
||||
'parentUrl' => $page->parent()->url(),
|
||||
'parentUrl' => $page->parent()->url()
|
||||
];
|
||||
|
||||
$pageData = array_merge($genericData, $specificData);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
<?php
|
||||
|
||||
$featured = $page->featured()->toPages()->first();
|
||||
|
||||
$mapArticle = function($article) {
|
||||
return [
|
||||
'title' => $article->title()->value(),
|
||||
'slug' => $article->slug(),
|
||||
'date' => $article->date()->toDate('d/m/Y'),
|
||||
'intro' => $article->intro()->excerpt(200),
|
||||
'cover' => $article->cover()->toFile()?->url(),
|
||||
];
|
||||
};
|
||||
|
||||
$articles = $page->children()->listed()->sortBy('date', 'desc');
|
||||
|
||||
$specificData = [
|
||||
'intro' => $page->intro()->value(),
|
||||
'featured' => $featured ? $mapArticle($featured) : null,
|
||||
'articles' => ($featured ? $articles->not($featured) : $articles)
|
||||
->map($mapArticle)
|
||||
->values(),
|
||||
'intro' => [
|
||||
'title' => $page->introTitle()->value(),
|
||||
'text' => $page->introText()->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'),
|
||||
'dateFormatted' => $article->date()->toDate('d/m/Y'),
|
||||
'intro' => $article->intro()->excerpt(200),
|
||||
'cover' => $article->cover()->toFile()?->url(),
|
||||
'authorName' => $article->authorName()->value(),
|
||||
'authorPhoto' => $article->authorPhoto()->toFile()?->url()
|
||||
];
|
||||
})->values()
|
||||
];
|
||||
|
||||
$pageData = array_merge($genericData, $specificData);
|
||||
|
|
|
|||
|
|
@ -8,39 +8,16 @@ function normalizePath(path) {
|
|||
return path === "/" ? "/home" : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve l'index de la slide correspondant au path.
|
||||
* Si le path exact n'existe pas, essaie le chemin parent
|
||||
* (ex: /blog/article-slug → /blog).
|
||||
*/
|
||||
function findSlideIndex(path) {
|
||||
let idx = slides.getIndexByPath(path);
|
||||
if (idx !== -1) return idx;
|
||||
const parentPath = path.replace(/\/[^/]+$/, "");
|
||||
if (parentPath) return slides.getIndexByPath(parentPath);
|
||||
return -1;
|
||||
}
|
||||
|
||||
async function loadSlide(path) {
|
||||
let slidePath = path;
|
||||
let idx = slides.getIndexByPath(slidePath);
|
||||
|
||||
// Sub-page: resolve to parent slide (ex: /blog/slug → /blog)
|
||||
if (idx === -1) {
|
||||
const parentPath = path.replace(/\/[^/]+$/, "");
|
||||
idx = slides.getIndexByPath(parentPath);
|
||||
if (idx !== -1) slidePath = parentPath;
|
||||
}
|
||||
|
||||
const idx = slides.getIndexByPath(path);
|
||||
if (idx !== -1) {
|
||||
const slide = slides.all[idx];
|
||||
if (slide.loaded || slide.loading) return;
|
||||
slides.setLoading(slidePath, true);
|
||||
slides.setLoading(path, true);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the actual slide path (parent), not the sub-page
|
||||
const response = await fetch(`${slidePath}.json`);
|
||||
const response = await fetch(`${path}.json`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
|
|
@ -51,10 +28,10 @@ async function loadSlide(path) {
|
|||
siteInitialized = true;
|
||||
}
|
||||
|
||||
slides.setData(slidePath, data);
|
||||
slides.setData(path, data);
|
||||
} catch (error) {
|
||||
console.error(`[router] Failed to load slide ${slidePath}:`, error);
|
||||
slides.setLoading(slidePath, false);
|
||||
console.error(`[router] Failed to load slide ${path}:`, error);
|
||||
slides.setLoading(path, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,17 +48,15 @@ export function slideTo(path, { skipHistory = false } = {}) {
|
|||
history.pushState({}, "", path === "/home" ? "/" : path);
|
||||
}
|
||||
|
||||
const idx = findSlideIndex(path);
|
||||
const slidePath = idx !== -1 ? slides.all[idx].path : path;
|
||||
|
||||
const idx = slides.getIndexByPath(path);
|
||||
if (idx !== -1 && slides.all[idx].title) {
|
||||
document.title = `${slides.all[idx].title} — World Game`;
|
||||
}
|
||||
|
||||
slides.slideTo(slidePath);
|
||||
slides.slideTo(path);
|
||||
|
||||
if (idx !== -1 && !slides.all[idx].loaded) {
|
||||
loadSlide(slidePath);
|
||||
loadSlide(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,14 +65,12 @@ export async function initRouter() {
|
|||
|
||||
await loadSlide(initialPath);
|
||||
|
||||
const idx = findSlideIndex(initialPath);
|
||||
const idx = slides.getIndexByPath(initialPath);
|
||||
if (idx !== -1) {
|
||||
slides.setActiveIndex(idx);
|
||||
}
|
||||
|
||||
loadAllSlidesInBackground(
|
||||
idx !== -1 ? slides.all[idx].path : initialPath
|
||||
);
|
||||
loadAllSlidesInBackground(initialPath);
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
const path = normalizePath(window.location.pathname);
|
||||
|
|
|
|||
|
|
@ -13,18 +13,6 @@
|
|||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Pages scrollables (blog, article, etc.) */
|
||||
.page-scrollable {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* Vertical Lines */
|
||||
.vertical-line {
|
||||
z-index: var(--z-base);
|
||||
|
|
|
|||
|
|
@ -1,262 +1,24 @@
|
|||
<script>
|
||||
/**
|
||||
* Vue article — sous-composant de Blog.svelte.
|
||||
* Reçoit les données article via props, pas via le slide system.
|
||||
*/
|
||||
let { data, onBack } = $props()
|
||||
import { fade } from 'svelte/transition'
|
||||
let { data } = $props()
|
||||
</script>
|
||||
|
||||
<article class="article">
|
||||
|
||||
<!-- Date + retour -->
|
||||
<div class="article-topbar">
|
||||
<time class="article-date">{data.date}</time>
|
||||
<button class="article-back" onclick={onBack}>← Retour</button>
|
||||
<div class="article" transition:fade>
|
||||
<div class="article__container">
|
||||
<h1>{data?.title || 'Article'}</h1>
|
||||
<p>Article view - To be implemented</p>
|
||||
</div>
|
||||
|
||||
<!-- Titre -->
|
||||
<h1 class="article-title font-face-terminal">{data.title}</h1>
|
||||
|
||||
<!-- Intro -->
|
||||
{#if data.intro}
|
||||
<div class="article-intro">{@html data.intro}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Cover -->
|
||||
{#if data.cover}
|
||||
<div class="article-cover">
|
||||
<img src={data.cover} alt={data.title} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Body (Kirby blocks → HTML) -->
|
||||
{#if data.body}
|
||||
<div class="article-body">{@html data.body}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Articles recommandés -->
|
||||
{#if data.related?.length}
|
||||
<section class="article-related">
|
||||
<h2>Nos recommandations</h2>
|
||||
<div class="article-related-grid">
|
||||
{#each data.related as rec}
|
||||
<a href="/blog/{rec.slug}" class="article-related-card">
|
||||
{#if rec.cover}
|
||||
<img src={rec.cover} alt={rec.title} />
|
||||
{/if}
|
||||
<span class="article-related-title">{rec.title}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.article {
|
||||
max-width: 900px;
|
||||
min-height: 100vh;
|
||||
padding: 8rem 2rem 4rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.article__container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 2rem 6rem;
|
||||
}
|
||||
|
||||
/* --- Topbar --- */
|
||||
.article-topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.article-date {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: var(--font-size-paragraph);
|
||||
}
|
||||
|
||||
.article-back {
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
font-family: "Danzza Medium", sans-serif;
|
||||
font-size: var(--font-size-paragraph-small);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.article-back:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* --- Titre --- */
|
||||
.article-title {
|
||||
font-size: var(--font-size-title-main);
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* --- Intro --- */
|
||||
.article-intro {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-subtitle);
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 2.5rem;
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* --- Cover --- */
|
||||
.article-cover {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.article-cover img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* --- Body (rich text from Kirby blocks) --- */
|
||||
.article-body {
|
||||
line-height: 1.8;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.article-body :global(h2),
|
||||
.article-body :global(h3) {
|
||||
font-family: "Danzza Bold", sans-serif;
|
||||
margin: 2rem 0 1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.article-body :global(h2) {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.article-body :global(h3) {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.article-body :global(p) {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: var(--font-size-paragraph);
|
||||
}
|
||||
|
||||
.article-body :global(a) {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.article-body :global(ul),
|
||||
.article-body :global(ol) {
|
||||
margin: 1.25rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.article-body :global(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.article-body :global(blockquote) {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
padding: 1.25rem 1.25rem 1.25rem 2rem;
|
||||
margin: 2rem 0;
|
||||
background: rgba(4, 254, 160, 0.05);
|
||||
}
|
||||
|
||||
.article-body :global(blockquote p) {
|
||||
font-style: italic;
|
||||
font-size: 19px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.article-body :global(figure) {
|
||||
margin: 2rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.article-body :global(figure img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.article-body :global(figcaption) {
|
||||
margin-top: 0.5rem;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
/* --- Recommandations --- */
|
||||
.article-related {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
.article-related h2 {
|
||||
font-family: "Danzza Bold", sans-serif;
|
||||
font-size: var(--font-size-title-section);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.article-related-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.article-related-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.article-related-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.article-related-card img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.article-related-title {
|
||||
font-family: "Danzza Bold", sans-serif;
|
||||
font-size: var(--font-size-paragraph);
|
||||
}
|
||||
|
||||
/* --- Mobile --- */
|
||||
@media (--mobile) {
|
||||
.article {
|
||||
padding: 3rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: var(--font-size-title-section-mobile);
|
||||
}
|
||||
|
||||
.article-intro {
|
||||
font-size: var(--font-size-paragraph-mobile);
|
||||
}
|
||||
|
||||
.article-related-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Tablet --- */
|
||||
@media (--tablet-only) {
|
||||
.article-title {
|
||||
font-size: var(--font-size-title-main-tablet);
|
||||
}
|
||||
|
||||
.article-related-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,350 +1,24 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { slides } from '@state/slides.svelte'
|
||||
import Footer from '@components/layout/Footer.svelte'
|
||||
import Article from '@views/Article.svelte'
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
let { data } = $props()
|
||||
|
||||
const isActive = $derived(slides.active?.id === 'blog')
|
||||
const featured = $derived(data?.featured ?? null)
|
||||
const articles = $derived(data?.articles ?? [])
|
||||
|
||||
// --- Article sub-page state ---
|
||||
let articleData = $state(null)
|
||||
let articleLoading = $state(false)
|
||||
let sectionEl = $state(null)
|
||||
|
||||
// Extract slug from current URL (for direct navigation to /blog/slug)
|
||||
function getSlugFromUrl() {
|
||||
const parts = window.location.pathname.split('/').filter(Boolean)
|
||||
// parts = ['blog', 'slug'] → slug = 'slug'
|
||||
return parts.length >= 2 && parts[0] === 'blog' ? parts[1] : null
|
||||
}
|
||||
|
||||
async function openArticle(slug) {
|
||||
articleLoading = true
|
||||
try {
|
||||
const res = await fetch(`/blog/${slug}.json`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
articleData = await res.json()
|
||||
scrollToTop()
|
||||
} catch (e) {
|
||||
console.error('[blog] Failed to load article:', e)
|
||||
articleData = null
|
||||
} finally {
|
||||
articleLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function backToList() {
|
||||
articleData = null
|
||||
history.pushState({}, '', '/blog')
|
||||
document.title = `${data?.title ?? 'Blog'} — World Game`
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
sectionEl?.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
// Handle article link clicks (intercepted BEFORE the router's document handler)
|
||||
function handleClick(e) {
|
||||
const link = e.target.closest('a[href]')
|
||||
if (!link) return
|
||||
|
||||
const url = new URL(link.href, window.location.origin)
|
||||
if (url.origin !== window.location.origin) return
|
||||
|
||||
const match = url.pathname.match(/^\/blog\/([^/]+)$/)
|
||||
if (match) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const slug = match[1]
|
||||
history.pushState({}, '', `/blog/${slug}`)
|
||||
openArticle(slug)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser back/forward within blog
|
||||
function handlePopState() {
|
||||
if (!isActive) return
|
||||
const slug = getSlugFromUrl()
|
||||
if (slug && (!articleData || articleData.uri !== `blog/${slug}`)) {
|
||||
openArticle(slug)
|
||||
} else if (!slug && articleData) {
|
||||
articleData = null
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
// On mount: check if URL is /blog/slug (direct navigation)
|
||||
onMount(() => {
|
||||
const slug = getSlugFromUrl()
|
||||
if (slug) openArticle(slug)
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
}
|
||||
})
|
||||
|
||||
// Reset article view when leaving the blog slide
|
||||
$effect(() => {
|
||||
if (!isActive && articleData) {
|
||||
articleData = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<section
|
||||
class="blog page-scrollable"
|
||||
bind:this={sectionEl}
|
||||
onclick={handleClick}
|
||||
>
|
||||
{#if articleData}
|
||||
<!-- Article view -->
|
||||
<Article data={articleData} onBack={backToList} />
|
||||
{:else}
|
||||
<!-- Blog list -->
|
||||
<div class="page-container">
|
||||
|
||||
{#if data?.intro}
|
||||
<header class="blog-header">
|
||||
{@html data.intro}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
{#if featured}
|
||||
<article class="blog-card blog-card--featured">
|
||||
<div class="blog-card-text">
|
||||
<time class="blog-card-date">{featured.date}</time>
|
||||
<h2 class="blog-card-title">
|
||||
<a href="/blog/{featured.slug}">{featured.title}</a>
|
||||
</h2>
|
||||
{#if featured.intro}
|
||||
<p class="blog-card-excerpt">{featured.intro}</p>
|
||||
{/if}
|
||||
<a href="/blog/{featured.slug}" class="blog-card-readmore">
|
||||
Lire l'article <span class="arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
{#if featured.cover}
|
||||
<div class="blog-card-image blog-card-image--featured">
|
||||
<a href="/blog/{featured.slug}">
|
||||
<img src={featured.cover} alt={featured.title} />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
<hr class="blog-divider" />
|
||||
{/if}
|
||||
|
||||
{#each articles as article, i}
|
||||
<article class="blog-card">
|
||||
<div class="blog-card-text">
|
||||
<time class="blog-card-date">{article.date}</time>
|
||||
<h2 class="blog-card-title">
|
||||
<a href="/blog/{article.slug}">{article.title}</a>
|
||||
</h2>
|
||||
<a href="/blog/{article.slug}" class="blog-card-readmore">
|
||||
Lire l'article <span class="arrow">→</span>
|
||||
</a>
|
||||
</div>
|
||||
{#if article.cover}
|
||||
<div class="blog-card-image">
|
||||
<a href="/blog/{article.slug}">
|
||||
<img src={article.cover} alt={article.title} />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
{#if i < articles.length - 1}
|
||||
<hr class="blog-divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if articleLoading}
|
||||
<p class="blog-loading">Chargement…</p>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
{/if}
|
||||
</section>
|
||||
<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 {
|
||||
background: #0d0e22;
|
||||
color: var(--color-text);
|
||||
min-height: 100vh;
|
||||
padding: 8rem 2rem 4rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Header / Intro --- */
|
||||
.blog-header {
|
||||
text-align: center;
|
||||
padding: 6rem 0 3rem;
|
||||
}
|
||||
|
||||
.blog-header :global(h1) {
|
||||
font-family: "Terminal", sans-serif;
|
||||
font-size: var(--font-size-title-main);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.blog-header :global(p) {
|
||||
font-size: var(--font-size-subtitle);
|
||||
line-height: 1.6;
|
||||
max-width: 640px;
|
||||
.blog__container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* --- Card (article item) --- */
|
||||
.blog-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.blog-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.blog-card-date {
|
||||
color: #d9d9d9;
|
||||
font-size: var(--font-size-paragraph);
|
||||
}
|
||||
|
||||
.blog-card-title {
|
||||
font-family: "Danzza Bold", sans-serif;
|
||||
font-size: var(--font-size-title-section);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.blog-card-title a {
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.blog-card-title a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.blog-card-excerpt {
|
||||
color: #d9d9d9;
|
||||
font-size: var(--font-size-paragraph);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.blog-card-readmore {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-primary);
|
||||
font-family: "Danzza Medium", sans-serif;
|
||||
font-size: var(--font-size-paragraph);
|
||||
transition: gap 0.2s;
|
||||
}
|
||||
|
||||
.blog-card-readmore:hover {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* --- Image --- */
|
||||
.blog-card-image img {
|
||||
width: 300px;
|
||||
height: 169px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.blog-card-image img:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.blog-card-image--featured img {
|
||||
width: auto;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* --- Divider --- */
|
||||
.blog-divider {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Featured --- */
|
||||
.blog-card--featured .blog-card-title {
|
||||
font-size: 36px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* --- Loading --- */
|
||||
.blog-loading {
|
||||
text-align: center;
|
||||
padding: 4rem 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* --- Mobile --- */
|
||||
@media (--mobile) {
|
||||
.blog-header {
|
||||
padding: 4rem 0 2rem;
|
||||
}
|
||||
|
||||
.blog-header :global(h1) {
|
||||
font-size: var(--font-size-title-main-mobile);
|
||||
}
|
||||
|
||||
.blog-header :global(p) {
|
||||
font-size: var(--font-size-subtitle-mobile);
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.blog-card-image img,
|
||||
.blog-card-image--featured img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.blog-card--featured .blog-card-title {
|
||||
font-size: var(--font-size-title-section-mobile);
|
||||
}
|
||||
|
||||
.blog-card-title {
|
||||
font-size: var(--font-size-title-section-mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Tablet --- */
|
||||
@media (--tablet-only) {
|
||||
.blog-header :global(h1) {
|
||||
font-size: var(--font-size-title-main-tablet);
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.blog-card-image img,
|
||||
.blog-card-image--featured img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue