Add WIP blog

This commit is contained in:
Tanguy Gérôme 2025-05-30 00:49:42 +03:00
parent d2702ae39f
commit 2d334db475
Signed by: tanguy
GPG key ID: 10B2947233740B88
7 changed files with 187 additions and 9 deletions

View file

@ -5,7 +5,7 @@ partial_translations: (partial translations)
resume: Resume resume: Resume
blog: Blog blog: Blog
welcome_blog: Welcome to my blog! blog_latest_posts: Latests posts
gallery: Gallery gallery: Gallery

View file

@ -5,7 +5,7 @@ partial_translations: (keskeneräisiä käännöksiä)
resume: Ansioluettelo resume: Ansioluettelo
blog: Blogi blog: Blogi
welcome_blog: Tervetuloa blogiini! blog_latest_posts: Viimeisimmät postaukset
gallery: Galleria gallery: Galleria

View file

@ -5,7 +5,7 @@ partial_translations: (traductions partielles)
resume: CV resume: CV
blog: Blog blog: Blog
welcome_blog: Bienvenue sur mon blog! blog_latest_posts: Dernier billets
gallery: Gallerie gallery: Gallerie

View file

@ -50,6 +50,10 @@ pub fn App() -> impl IntoView {
<I18nRoute<Locale, _, _> view=Outlet> <I18nRoute<Locale, _, _> view=Outlet>
<Route path=path!("/") view=HomePage/> <Route path=path!("/") view=HomePage/>
<Route path=path!("/resume") view=Resume/> <Route path=path!("/resume") view=Resume/>
<ParentRoute path=path!("/blog") view=crate::blog::Blog>
<Route path=path!(":slug") view=crate::blog::BlogPost/>
<Route path=path!("") view=|| view! {}/>
</ParentRoute>
<ParentRoute path=path!("/gallery") view=crate::gallery::Gallery> <ParentRoute path=path!("/gallery") view=crate::gallery::Gallery>
<Route path=path!(":slug") view=crate::gallery::GalleryEntry/> <Route path=path!(":slug") view=crate::gallery::GalleryEntry/>
<Route path=path!("") view=|| view! {}/> <Route path=path!("") view=|| view! {}/>
@ -92,10 +96,9 @@ pub fn Header() -> impl IntoView {
<h1><span>tanguy</span><span>.gerome</span><span>.fi</span></h1> <h1><span>tanguy</span><span>.gerome</span><span>.fi</span></h1>
<div class="links"> <div class="links">
<A href="/">{t!(i18n, home)}</A> <A href="/">{t!(i18n, home)}</A>
<A href="/resume">{t!(i18n, resume)}</A> <A href="/blog">{t!(i18n, blog)}</A>
// <A href="/blog">{t!(i18n, blog)}</A>
<A href="/gallery">{t!(i18n, gallery)}</A> <A href="/gallery">{t!(i18n, gallery)}</A>
// <A href="/resume">{t!(i18n, resume)}</A> <A href="/resume">{t!(i18n, resume)}</A>
</div> </div>
</div> </div>
<picture> <picture>

113
src/blog.rs Normal file
View file

@ -0,0 +1,113 @@
use leptos::{
Params,
prelude::*
};
use serde::{Deserialize, Serialize};
use leptos_router::{
components::Outlet, hooks::use_params, params::Params
};
use crate::{i18n::*, services::directus::Asset};
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct BlogPostTranslations {
title: String,
content: String
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlogPost {
slug: String,
translations: Option<BlogPostTranslations>,
illustration: Asset,
}
#[server(GetBlog, "/api", "GetJson")]
pub async fn get_blog_posts(locale: String) -> Result<Vec<BlogPost>, ServerFnError> {
crate::services::directus::get_client()
.get_all_items_in_collection::<BlogPost>(&locale, "blog_posts").await
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[component]
pub fn Blog() -> impl IntoView {
let i18n = use_i18n();
let blog_posts = Resource::new(move || i18n.get_locale(), move |locale| get_blog_posts(locale.to_string()));
view! {
<Outlet/>
<main class="main-width blog-wrapper">
<h2>{t!(i18n, blog_latest_posts)}:</h2>
<Suspense fallback=move || view! { <div>"Loading..."</div> }>
{move || blog_posts.get()
.and_then(|blog_posts| blog_posts.ok())
.and_then(|blog_posts| {
view! {
<div class="blog-entries">
{
blog_posts.into_iter().map(|post| {
view! {
<a class="blog-thumbnail" href={format!("/blog/{}", post.slug)}>
<img src=format!("https://directus.gerome.fi/assets/{}?width=600&height=400&fit=cover&format=auto&quality=80&withoutEnlargement=true", post.illustration.id)/>
<p class="blog-title">{post.translations.unwrap_or_default().title}</p>
</a>
}
}).collect::<Vec<_>>()
}
</div>
}.into()
})
}
</Suspense>
</main>
}
}
#[derive(Params, PartialEq)]
struct BlogPostParams {
slug: Option<String>,
}
#[component]
pub fn BlogPost() -> impl IntoView {
let i18n = use_i18n();
let params = use_params::<BlogPostParams>();
let slug = move || {
params .read()
.as_ref()
.ok()
.and_then(|params| params.slug.clone())
.unwrap_or_default()
};
let blog_posts = Resource::new(move || i18n.get_locale(), move |locale| get_blog_posts(locale.to_string()));
view! {
<Suspense fallback=move || view! { <main class="main-width blog-wrapper"><div>"Loading..."</div></main> }>
{move || blog_posts.get()
.and_then(|blog_posts| blog_posts.ok())
.and_then(|blog_posts| blog_posts.iter()
.find(|post| post.slug == slug())
.map(|post| post.clone()))
.and_then(|post| {
let translations = post.translations.unwrap_or_default();
let html = markdown::to_html(&translations.content);
view! {
<div class="blog-open-entry">
<div class="blog-illustration">
<img src=format!("https://directus.gerome.fi/assets/{}?width=1600&height=1000&fit=inside&format=auto&quality=90&withoutEnlargement=true", post.illustration.id)/>
<h2 class="blog-title">{translations.title}</h2>
</div>
<main class="main-width blog-wrapper">
<div class="markdown" inner_html=html/>
</main>
</div>
}.into()
})
}
</Suspense>
}
}

View file

@ -4,6 +4,7 @@ pub mod app;
pub mod services; pub mod services;
pub mod blog;
pub mod gallery; pub mod gallery;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]

View file

@ -233,7 +233,7 @@ body {
} }
} }
.gallery-entries { .gallery-entries, .blog-entries {
width: fit-content; width: fit-content;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -243,10 +243,71 @@ body {
gap: 16px; gap: 16px;
} }
.blog-open-entry {
.blog-illustration {
position: relative;
img {
max-height: 60vh;
width: 100vw;
max-width: 100vw;
object-fit: cover;
}
.blog-title {
display: block;
width: 100%;
position: absolute;
bottom: 0px;
padding: 16px;
padding-top: 64px;
margin: 0;
background: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 1.0));
color: white;
}
}
}
.blog-wrapper {
max-width: 1250px!important;
width: fit-content;
}
.blog-thumbnail {
position: relative;
img {
height: 100%;
width: 100%;
max-height: 300px;
max-width: 500px;
object-fit: contain;
}
.blog-title {
text-align: center;
display: block;
width: 100%;
position: absolute;
bottom: 0px;
padding: 16px;
padding-top: 64px;
margin: 0;
background: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 1.0));
color: white;
}
}
.gallery-wrapper {
max-width: 1000px!important;
width: fit-content;
}
.gallery-thumbnail { .gallery-thumbnail {
img { img {
height: 400px; height: 300px;
width: 400px; width: 300px;
object-fit: contain; object-fit: contain;
@media all and (max-width: 1000px) { @media all and (max-width: 1000px) {