Add Gallery + WIP blog

This commit is contained in:
Tanguy Gérôme 2024-11-09 22:49:25 +02:00
parent f1c4111e1d
commit 68ca7284fb
22 changed files with 1725 additions and 75 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
.env
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target/ /target/

898
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,11 +16,17 @@ leptos_router = { version = "0.6", features = ["nightly"] }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true } tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", features = ["util"], optional = true } tower = { version = "0.4", features = ["util"], optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.93" wasm-bindgen = "=0.2.95"
thiserror = "1" thiserror = "1"
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
http = "1" http = "1"
leptos_i18n = { version = "0.4.1", default-features = false, features = ["yaml_files"] } leptos_i18n = { version = "0.4.1", default-features = false, features = ["yaml_files"] }
serde = "1.0.213"
dotenvy = "0.15.7"
serde_json = "1.0.132"
serde_path_to_error = "0.1.16"
reqwest = { version = "0.12.9", features = ["json"]}
contentful = "0.8.0"
[features] [features]
hydrate = [ hydrate = [
@ -44,7 +50,8 @@ ssr = [
[package.metadata.leptos-i18n] [package.metadata.leptos-i18n]
default = "en" default = "en"
locales = ["en", "fr", "fi", "et", "sv", "eo", "jbo"] # locales = ["en", "fr", "fi", "et", "sv", "eo", "jbo"]
locales = ["en"]
# Defines a size-optimized profile for the WASM bundle in release mode # Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release] [profile.wasm-release]

View file

@ -2,5 +2,11 @@ home: Home
available_in: "Available in:" available_in: "Available in:"
partial_translations: (partial translations) partial_translations: (partial translations)
blog: Blog
welcome_blog: Welcome to my blog!
gallery: Gallery
gallery_title: Gallery
gallery_description: aaa
tanguy_gerome: Tanguy Gérôme tanguy_gerome: Tanguy Gérôme

View file

@ -1,6 +1,10 @@
home: Hejmo home: Hejmo
available_in: "Havebla en:" available_in: "Havebla en:"
partial_translations: partial_translations: (malkompletaj tradukoj)
blog: Blogo
welcome_blog: Bonvenon al mia blogo!
gallery: Galerio
tanguy_gerome: tanguy_gerome:

View file

@ -1,6 +1,10 @@
home: Kodu home: Kodu
available_in: "Saadaval keeles:" available_in: "Saadaval keeles:"
partial_translations: partial_translations: (puudulikud tõlked)
blog: Blogi
welcome_blog: Tere tulemast minu blogisse!
gallery: Galerii
tanguy_gerome: tanguy_gerome:

View file

@ -2,5 +2,9 @@ home: Koti
available_in: "Saatavilla kielillä:" available_in: "Saatavilla kielillä:"
partial_translations: (keskeneräisiä käännöksiä) partial_translations: (keskeneräisiä käännöksiä)
blog: Blogi
welcome_blog: Tervetuloa blogiini!
gallery: Galleria
tanguy_gerome: tanguy_gerome:

View file

@ -2,5 +2,9 @@ home: Accueil
available_in: "Disponible en:" available_in: "Disponible en:"
partial_translations: (traductions partielles) partial_translations: (traductions partielles)
blog: Blog
welcome_blog: Bienvenue sur mon blog!
gallery: Gallerie
tanguy_gerome: tanguy_gerome:

View file

@ -2,5 +2,9 @@ home: zdani
available_in: "fanva fi " available_in: "fanva fi "
partial_translations: (nalmu'o puvyfanva) partial_translations: (nalmu'o puvyfanva)
blog: snukarni
welcome_blog: .ui do zanvi'e fi le mi snukarni
gallery: larmuzga
tanguy_gerome: .tangis.jerom. tanguy_gerome: .tangis.jerom.

View file

@ -1,6 +1,10 @@
home: Hem home: Hem
available_in: "Finns på:" available_in: "Finns på:"
partial_translations: partial_translations: (saknade översättningar)
blog: Blogg
welcome_blog: Välkommen till min blogg!
gallery: Galleri
tanguy_gerome: tanguy_gerome:

View file

@ -1,7 +1,14 @@
buildImage = 'ghcr.io/railwayapp/nixpacks:debian-1730765032'
[phases.setup]
# aptPkgs = ['libc6']
[phases.install] [phases.install]
dependsOn = ['setup']
# nixPkgs = ['cargo-leptos'] # nixPkgs = ['cargo-leptos']
# cmds = ['cargo install -f wasm-bindgen-cli --version 0.2.93'] # cmds = ['cargo install -f wasm-bindgen-cli --version 0.2.93']
cmds = ['cargo install cargo-quickinstall', 'cargo quickinstall cargo-leptos'] cmds = ['cargo install cargo-quickinstall', 'cargo quickinstall cargo-leptos']
# cmds = ['cargo install cargo-leptos']
# cmds = ['rustup target add wasm32-unknown-unknown'] # cmds = ['rustup target add wasm32-unknown-unknown']
[phases.build] [phases.build]

View file

@ -1,10 +1,13 @@
leptos_i18n::load_locales!();
use i18n::*;
use leptos::*; use leptos::*;
use leptos_meta::*; use leptos_meta::*;
use leptos_router::*; use leptos_router::*;
use crate::i18n::*;
use crate::error_template::{AppError, ErrorTemplate}; use crate::error_template::{AppError, ErrorTemplate};
// use crate::services::contentful::get_rich_text_page;
// use crate::services::rich_text;
use crate::routes::gallery;
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
@ -32,8 +35,14 @@ pub fn App() -> impl IntoView {
}> }>
<Header/> <Header/>
<Routes> <Routes>
<I18nRoute view=|| view! { <Outlet /> }> <I18nRoute view=|| view! { <Outlet/> }>
<Route path="" view=HomePage/> <Route path="" view=HomePage/>
// <Route path="/blog" view=BlogList/>
// <Route path="/blog/:slug" view=BlogPost/>
<Route path="/gallery" view=gallery::Gallery>
<Route path=":slug" view=gallery::GalleryEntry/>
<Route path="" view=|| view! {}/>
</Route>
</I18nRoute> </I18nRoute>
</Routes> </Routes>
<Footer/> <Footer/>
@ -50,19 +59,19 @@ pub fn LanguageSwitcher() -> impl IntoView {
<div class="language-switcher main-width"> <div class="language-switcher main-width">
<p>{t!(i18n, available_in)}</p> <p>{t!(i18n, available_in)}</p>
<button class="link" on:click=move |_| i18n.set_locale(Locale::en)>english</button> <button class="link" on:click=move |_| i18n.set_locale(Locale::en)>english</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::fr)>français</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::fr)>français</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::fi)>suomi</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::fi)>suomi</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::et)>eesti</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::et)>eesti</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::sv)>svenska</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::sv)>svenska</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::eo)>esperanto</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::eo)>esperanto</button>
<button class="link" on:click=move |_| i18n.set_locale(Locale::jbo)>lojban</button> // <button class="link" on:click=move |_| i18n.set_locale(Locale::jbo)>lojban</button>
<p>{t!(i18n, partial_translations)}</p> <p>{t!(i18n, partial_translations)}</p>
</div> </div>
} }
} }
#[component] #[component]
fn Header() -> impl IntoView { pub fn Header() -> impl IntoView {
let i18n = use_i18n(); let i18n = use_i18n();
view! { view! {
@ -71,7 +80,9 @@ fn Header() -> impl IntoView {
<div class="text"> <div class="text">
<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="/blog">{t!(i18n, blog)}</A>
<A href="/gallery">{t!(i18n, gallery)}</A>
</div> </div>
</div> </div>
<img <img
@ -81,13 +92,13 @@ fn Header() -> impl IntoView {
/> />
</div> </div>
<LanguageSwitcher/> // <LanguageSwitcher/>
</header> </header>
} }
} }
#[component] #[component]
fn Footer() -> impl IntoView { pub fn Footer() -> impl IntoView {
let i18n = use_i18n(); let i18n = use_i18n();
view! { view! {
@ -105,11 +116,41 @@ fn Footer() -> impl IntoView {
} }
#[component] #[component]
fn HomePage() -> impl IntoView { pub fn HomePage() -> impl IntoView {
view! { view! {
<main class="main-width"> <main class="main-width">
<h1>"Welcome!"</h1> <h1>Welcome!</h1>
<p>"This website is under construction, please check back later :)"</p>
<p>"I'm "<b>Tanguy Gérôme</b>, a 26 year old software developer, amateur photographer, and over all nerd living in Helsinki, Finland, originally from Cornimont, France.</p>
<p>
On here you can find my personal photography portfolio, and (coming) a blog where I scribble about whatever comes to mind in relation to my hobbies and interests, including:
<ul>
<li>Linux</li>
<li>video games</li>
<li>knitting</li>
<li>hiking</li>
<li>scouting</li>
<li>language learning</li>
<li>photography (digital and film)</li>
</ul>
</p>
<p>This website is still under construction, more content to come (hopefully) soon.</p>
// <Await future=move || get_rich_text_page("home-page".to_string()) let:page>
// {match page {
// Ok(page) => {
// view! {
// <div>
// {rich_text::document_handler(&page.english.rich_text_content)}
// </div>
// }
// },
// Err(error) => view! { <div><p>"Error: "{error.to_string()}</p></div> }
// }}
// </Await>
</main> </main>
} }
} }

103
src/blog.rs Normal file
View file

@ -0,0 +1,103 @@
use leptos::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
use crate::i18n::*;
use crate::services::contentful::get_rich_text_page;
use crate::services::rich_text;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlogPostMetadata {
title: String,
page_slug: String,
}
#[derive(Params, PartialEq)]
struct BlogPostParams {
slug: String
}
// #[server(GetBlogPostList, "/api", "GetJson")]
// pub async fn get_blog_post_list() -> Result<Vec<BlogPostMetadata>, ServerFnError> {
// use crate::services::contentful;
// let contentful_client = contentful::get_contentful_client();
// let pages = contentful_client.get_entries_by_type::<contentful::RichTextPageLanguagesWrapper>("richTextPageLanguagesWrapper", None).await;
// match pages {
// Ok(found_pages) => {
// Ok(found_pages.into_iter().map(|page| BlogPostMetadata { title: page.english.title, page_slug: page.english.page_slug }).collect())
// },
// Err(e) => Err(ServerFnError::ServerError(e.to_string())),
// }
// }
#[component]
pub fn BlogList() -> impl IntoView {
let i18n = use_i18n();
// let blog_pages_local = create_local_resource(|| (), |_| async move { get_blog_post_list().await });
// let blog_pages = create_resource(|| (), |_| async move { get_blog_post_list().await });
view! {
<main class="main-width">
<h1>{t!(i18n, welcome_blog)}</h1>
// <Await future=|| get_blog_post_list() let:pages>
// <p>"Loaded " {pages.as_ref().unwrap().len()} " pages"</p>
// </Await>
// <Suspense fallback=move || view! { <p>"Loading..."</p> }>
// {move || blog_pages_local.get()
// .map(|pages| view! { <p>"Loaded " {pages.unwrap().len()} " pages"</p> })
// }
// </Suspense>
// <Suspense fallback=move || view! { <p>"Loading..."</p> }>
// {match blog_pages.get().unwrap_or(Ok(Vec::new())) {
// Ok(pages) => {
// view! {
// <ul>
// {pages.into_iter()
// .map(|page| {
// let blog_post_url = format!("/blog/{}", page.page_slug);
// view! { <li><A href=blog_post_url>{page.title}</A></li> }
// })
// .collect::<Vec<_>>()}
// </ul>
// }
// },
// Err(error) => view! { <ul>"Error: "{error.to_string()}</ul> }
// }}
// {move || blog_pages.get()
// .map(|pages| view! { <p>"Found " {pages.unwrap().len()} " pages"</p> })
// }
// </Suspense>
</main>
}
}
#[component]
pub fn BlogPost() -> impl IntoView {
let params = use_params::<BlogPostParams>();
let slug= move || {
params.with(|params| params.as_ref()
.map(|params| params.slug.clone())
.unwrap())
};
let blog_page = create_resource(|| (), move |_| async move { get_rich_text_page(slug()).await });
view! {
<main class="main-width">
<Suspense fallback=move || view! { <div>"Loading..."</div> }>
{match blog_page.get().unwrap_or(Err(ServerFnError::ServerError("Loading...".to_string()))) {
Ok(page) => {
view! {
<div>
<h1>{page.english.title}</h1>
{rich_text::document_handler(&page.english.rich_text_content)}
</div>
}
},
Err(error) => view! { <div>"Error: "{error.to_string()}</div> }
}}
</Suspense>
</main>
}
}

0
src/components/mod.rs Normal file
View file

View file

@ -1,5 +1,12 @@
leptos_i18n::load_locales!();
pub mod app; pub mod app;
pub mod blog;
pub mod error_template; pub mod error_template;
pub mod components;
pub mod routes;
pub mod services;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub mod fileserv; pub mod fileserv;

View file

@ -1,6 +1,9 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
if cfg!(debug_assertions) {
let _ = dotenvy::dotenv();
}
use axum::Router; use axum::Router;
use leptos::*; use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_axum::{generate_route_list, LeptosRoutes};

128
src/routes/gallery.rs Normal file
View file

@ -0,0 +1,128 @@
use leptos::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
// use crate::i18n::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatermarkedImage {
title: String,
slug: String,
#[serde(default)]
description: String,
image_without_watermark: contentful::models::Asset,
image_with_watermark: contentful::models::Asset
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Gallery {
title: String,
slug: String,
#[serde(default)]
description: String,
images: Vec<WatermarkedImage>
}
#[server(GetGallery, "/api", "GetJson")]
pub async fn get_gallery() -> Result<Gallery, ServerFnError> {
use crate::services::contentful;
let contentful_client = contentful::get_contentful_client();
let slug = "photography-portfolio";
let query_string = format!("?content_type=imageGallery&include=10&fields.slug={}", slug);
let galleries = contentful_client.get_entries_by_query_string::<Gallery>(&query_string).await;
match galleries {
Ok(found_galleries) => {
if found_galleries.len() != 1 {
return Err(ServerFnError::ServerError("Found none or more than one".to_string()));
}
Ok(found_galleries[0].clone())
},
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
#[derive(Params, PartialEq)]
struct GalleryEntryParams {
slug: String
}
#[component]
pub fn GalleryEntry() -> impl IntoView {
let params = use_params::<GalleryEntryParams>();
let slug= move || {
params.with(|params| params.as_ref()
.map(|params| params.slug.clone())
.unwrap_or_default())
};
let gallery = create_resource(|| (), |_| async move { get_gallery().await });
view! {
<Suspense fallback=move || view! { <p>"Loading..."</p> }>
{move || {
gallery.get().map(|gallery_loaded|
match gallery_loaded {
Ok(gallery) => {
let image = move || gallery.images.into_iter()
.find(move |image| image.slug == slug());
match image() {
None => view! { <div/> },
Some(image) => {
let display_url = format!("https:{}?w=2400&h=2400&q=80&fm=avif", &image.image_with_watermark.file.url);
view! {
<div class="open-entry">
<img src=display_url alt=image.title.clone()/>
<h3>{image.title.clone()}</h3>
<p>{image.description}</p>
</div>
}
}
}
},
Err(error) => view! { <div>"Error: "{error.to_string()}</div> }
}
)
}}
</Suspense>
}
}
#[component]
pub fn Gallery() -> impl IntoView {
let gallery = create_resource(|| (), |_| async move { get_gallery().await });
view! {
<main class="main-width gallery-wrapper">
<Outlet/>
<Suspense fallback=move || view! { <p>"Loading..."</p> }>
{move || {
gallery.get().map(|gallery_loaded|
match gallery_loaded {
Ok(gallery) => {
view! {
<div class="gallery">
<h3>{gallery.title}</h3>
<p>{gallery.description}</p>
<ul>
{gallery.images.into_iter()
.map(|image| {
let thumbnail_url = format!("https:{}?w=800&h=800&q=80&fm=avif", &image.image_without_watermark.file.url);
let link_url = format!("/gallery/{}", &image.slug);
view! { <li><A href=link_url><img src=thumbnail_url alt=image.title/></A></li> }
}).collect::<Vec<_>>()
}
</ul>
</div>
}
},
Err(error) => view! { <div>"Error: "{error.to_string()}</div> }
}
)
}}
</Suspense>
</main>
}
}

2
src/routes/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod gallery;

268
src/services/contentful.rs Normal file
View file

@ -0,0 +1,268 @@
use std::env;
// use reqwest::StatusCode;
use leptos::*;
use serde::{Deserialize, Serialize};
use serde_json;
// use crate::services::rich_text;
//pub struct Client {
// access_token: String,
// space_id: String,
// base_url: String,
// environment_id: String,
//}
//impl Client {
// pub fn new(
// access_token: &str,
// space_id: &str,
// base_url: &str,
// environment_id: &str,
// ) -> Client {
// Client {
// base_url: base_url.into(),
// access_token: access_token.into(),
// space_id: space_id.into(),
// environment_id: environment_id.into(),
// }
// }
// fn get_entries_url(&self, query_string: &str) -> String {
// format!(
// "{base_url}/{space_id}/environments/{environment}/entries{query_string}",
// base_url = &self.base_url,
// space_id = &self.space_id,
// environment = &self.environment_id,
// query_string = &query_string
// )
// }
// pub async fn get_entries_by_query_string<T>(
// &self,
// query_string: &str
// ) -> Result<Vec<T>, Box<dyn std::error::Error>>
// where
// for<'a> T: Serialize + Deserialize<'a>,
// {
// let url = self.get_entries_url(query_string);
// let reqwest_client = reqwest::Client::new();
// let request = reqwest_client.get(&url).bearer_auth(&self.access_token);
// let response = request.send().await?;
// let json_response = match response.status() {
// StatusCode::OK => {
// let json = response.json::<serde_json::Value>().await?;
// Some(json)
// },
// StatusCode::NOT_FOUND => None,
// _ => {
// unimplemented!();
// },
// };
// match json_response {
// Some(json) => {
// if let Some(items) = json.clone().get_mut("items") {
// if items.is_array() {
// if let Some(includes) = json.get("includes") {
// self.resolve_array(items, includes)?;
// } else {
// let includes = serde_json::Value::default();
// self.resolve_array(items, &includes)?;
// }
// let ar_string = items.to_string();
// let entries = serde_json::from_str::<Vec<T>>(ar_string.as_str())?;
// Ok(entries)
// } else {
// unimplemented!();
// }
// } else {
// unimplemented!();
// }
// },
// _ => unimplemented!(),
// }
// }
// fn resolve_array(
// &self,
// value: &mut serde_json::Value,
// includes: &serde_json::Value,
// ) -> Result<(), Box<dyn std::error::Error>> {
// let items = value.as_array_mut().unwrap();
// for item in items {
// if item.is_object() {
// self.resolve_object(item, includes)?;
// } else if item.is_string() || item.is_number() {
// // do nothing
// } else {
// unimplemented!();
// }
// }
// Ok(())
// }
// fn resolve_object(
// &self,
// value: &mut serde_json::Value,
// includes: &serde_json::Value,
// ) -> Result<(), Box<dyn std::error::Error>> {
// if let Some(sys) = value.get("sys") {
// if let Some(sys_type) = sys.get("type") {
// if sys_type == "Entry" {
// self.resolve_entry(value, includes)?;
// } else if sys_type == "Link" {
// self.resolve_link(value, includes)?;
// } else {
// let node_type = value["nodeType"].clone();
// if node_type == "document" {
// // log::warn!("TODO: Richtext is not yet implemented");
// } else {
// unimplemented!(
// "{} - {} not implemented for {}",
// &sys_type,
// &node_type,
// &value
// );
// }
// }
// } else {
// unimplemented!("sys.type do not exist, though sys exists") // TODO: Can this ever happen?
// }
// } else {
// // Do nothing, as it likely a json object
// }
// Ok(())
// }
// fn resolve_asset(&self, value: &mut serde_json::Value) -> Result<(), Box<dyn std::error::Error>> {
// if let Some(fields) = value.get_mut("fields") {
// if fields.is_object() {
// *value = fields.clone();
// } else {
// unimplemented!();
// }
// } else {
// unimplemented!();
// }
// Ok(())
// }
// fn resolve_entry(
// &self,
// value: &mut serde_json::Value,
// includes: &serde_json::Value,
// ) -> Result<(), Box<dyn std::error::Error>> {
// if let Some(fields) = value.get_mut("fields") {
// if fields.is_object() {
// let entry_object = fields.as_object_mut().unwrap();
// for (_field_name, field_value) in entry_object {
// if field_value.is_object() {
// self.resolve_object(field_value, includes)?;
// } else if field_value.is_array() {
// self.resolve_array(field_value, includes)?;
// } else {
// // Regular string, number, etc, values. No need to do anything.
// }
// }
// *value = fields.clone();
// } else {
// unimplemented!();
// }
// } else {
// unimplemented!();
// }
// Ok(())
// }
// fn resolve_link(
// &self,
// value: &mut serde_json::Value,
// includes: &serde_json::Value,
// ) -> Result<(), Box<dyn std::error::Error>> {
// let link_type = value["sys"]["linkType"].clone();
// let link_id = value["sys"]["id"].clone();
// if link_type == "Entry" {
// let included_entries = includes["Entry"].as_array().unwrap();
// let mut filtered_entries = included_entries
// .iter()
// .filter(|entry| entry["sys"]["id"] == link_id)
// .take(1);
// let linked_entry = filtered_entries.next();
// if let Some(entry) = linked_entry {
// let mut entry = entry.clone();
// self.resolve_entry(&mut entry, includes)?;
// *value = entry;
// //value["fields"] = entry["fields"].clone();
// //*value = entry["fields"].clone();
// }
// } else if link_type == "Asset" {
// let included_assets = includes["Asset"].as_array().unwrap();
// let mut filtered_assets = included_assets
// .iter()
// .filter(|entry| entry["sys"]["id"] == link_id)
// .take(1);
// let linked_asset = filtered_assets.next();
// if let Some(asset) = linked_asset {
// let mut asset = asset.clone();
// self.resolve_asset(&mut asset)?;
// *value = asset;
// }
// } else {
// unimplemented!();
// }
// //*value = value["fields"].clone();
// Ok(())
// }
//}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichTextPage {
pub title: String,
pub page_slug: String,
// pub rich_text_content: rich_text::RichTextNode,
pub rich_text_content: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichTextPageLanguagesWrapper {
pub internal_title: String,
pub english: RichTextPage,
}
#[server(GetRichTextPage, "/api", "GetJson")]
pub async fn get_rich_text_page(slug: String) -> Result<RichTextPageLanguagesWrapper, ServerFnError> {
let contentful_client = get_contentful_client();
// let builder = contentful::QueryBuilder::new()
// .content_type_is("richTextPageLanguagesWrapper")
// .include(10)
// .field_equals("fields.slug", &slug);
let query_string = format!("?content_type=richTextPageLanguagesWrapper&include=10&fields.slug={}", slug);
let pages = contentful_client.get_entries_by_query_string::<RichTextPageLanguagesWrapper>(&query_string).await;
match pages {
Ok(found_pages) => {
if found_pages.len() != 1 {
return Err(ServerFnError::ServerError("Found none or more than one".to_string()));
}
let english_rich_text_content = found_pages[0].english.rich_text_content.clone();
dbg!(english_rich_text_content);
Ok(found_pages[0].clone())
},
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
pub fn get_contentful_client() -> contentful::ContentfulClient {
let access_token = env::var("CONTENTFUL_ACCESS_TOKEN").unwrap();
const SPACE_ID: &str = "e3magj9g6dp1";
// let base_url = "https://cdn.contentful.com/spaces";
// const ENVIRONMENT_ID: &str = "master";
// return Client::new(&access_token, SPACE_ID, base_url, ENVIRONMENT_ID);
return contentful::ContentfulClient::new(&access_token, SPACE_ID);
}

2
src/services/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod contentful;
pub mod rich_text;

193
src/services/rich_text.rs Normal file
View file

@ -0,0 +1,193 @@
use leptos::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum RichTextNode {
Block {
node_type: String,
// data: Option<serde_json::Value>,
content: Vec<RichTextNode>,
},
Text {
node_type: String,
// data: Option<serde_json::Value>,
value: String,
// marks: Vec<Mark>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextBlock {
node_type: String,
// data: Option<serde_json::Value>,
value: String,
// marks: Vec<Mark>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Mark {
#[serde(rename = "type")]
pub mark_type: String
}
#[derive(Debug, Clone)]
struct NodeHandler {
node_type: &'static str,
function: fn (node: &Value) -> HtmlElement<html::AnyElement>,
}
const NODE_HANDLERS: [NodeHandler; 12] = [
NodeHandler { node_type: "document", function: document_handler },
NodeHandler { node_type: "paragraph", function: paragraph_handler },
NodeHandler { node_type: "text", function: text_handler },
NodeHandler { node_type: "heading-1", function: heading_1_handler },
NodeHandler { node_type: "heading-2", function: heading_2_handler },
NodeHandler { node_type: "heading-3", function: heading_3_handler },
NodeHandler { node_type: "heading-4", function: heading_4_handler },
NodeHandler { node_type: "heading-5", function: heading_5_handler },
NodeHandler { node_type: "heading-6", function: heading_6_handler },
NodeHandler { node_type: "hr", function: hr_handler },
NodeHandler { node_type: "blockquote", function: quote_handler },
NodeHandler { node_type: "embedded-asset-block", function: asset_handler },
];
pub fn document_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! {
<div class="rich-text-document">
{render_children(&children)}
</div>
}.into()
}
fn paragraph_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! {
<p>
{render_children(&children)}
</p>
}.into()
}
fn text_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let block: TextBlock = serde_json::from_value(node.clone()).unwrap();
let text = block.value.replace("\n", "<br/>");
view! { <span inner_html=text>{}</span> }.into()
}
fn heading_1_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h1> {render_children(&children)} </h1> }.into()
}
fn heading_2_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h2> {render_children(&children)} </h2> }.into()
}
fn heading_3_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h3> {render_children(&children)} </h3> }.into()
}
fn heading_4_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h4> {render_children(&children)} </h4> }.into()
}
fn heading_5_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h5> {render_children(&children)} </h5> }.into()
}
fn heading_6_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <h6> {render_children(&children)} </h6> }.into()
}
fn hr_handler(_node: &Value) -> HtmlElement<html::AnyElement> {
view! { <hr/> }.into()
}
fn quote_handler(node: &Value) -> HtmlElement<html::AnyElement> {
let children: Vec<Value> = serde_json::from_value(node["content"].clone()).unwrap();
view! { <blockquote> {render_children(&children)} </blockquote> }.into()
}
fn asset_handler(_node: &Value) -> HtmlElement<html::AnyElement> {
// let data = node["data"];
// let img_url = format!(
// "https://images.ctfassets.net/e3magj9g6dp1/14q5L7K0BCol1gx0aCSCck/d1f69bfa404efed6a2dcc71401bbc16d/P5310039-1-2.jpg?w=1600&q=50&fm=avif"
// );
view! { <img> </img> }.into()
}
fn empty_handler() -> HtmlElement<html::AnyElement> {
view! { <div/> }.into()
}
fn node_dispatcher(node: &Value) -> HtmlElement<html::AnyElement> {
for handler in NODE_HANDLERS.iter() {
let node_type = node["nodeType"].clone();
if handler.node_type == node_type {
return (handler.function)(node)
}
}
empty_handler()
}
fn render_children(children: &Vec<Value>) -> Vec<HtmlElement<html::AnyElement>> {
children.iter().map(|child| node_dispatcher(child)).collect()
}
// pub fn render_rich_text(rich_text: Value) -> HtmlElement<html::AnyElement> {
// node_dispatcher(&rich_text)
// }
// fn _document_handler(node: &RichTextNode) -> HtmlElement<html::AnyElement> {
// let empty_vec: Vec<RichTextNode> = Vec::new();
// let children = match node {
// RichTextNode::Block{node_type: _, content} => content,
// // RichTextNode::Block{node_type: _, data: _, content} => content,
// _ => &empty_vec,
// };
// view! {
// <div class="rich-text-document">
// {_render_children(children)}
// </div>
// }.into()
// }
// fn _text_handler(node: &RichTextNode) -> HtmlElement<html::AnyElement> {
// match node {
// RichTextNode::Text{node_type: _, value} => view! { <span>{value}</span> }.into(),
// // RichTextNode::Text{node_type: _, data: _, value, marks} => view! { <span>{value}</span> }.into(),
// _ => view! { <span>Error</span> }.into(),
// }
// }
// fn _node_dispatcher(node: &RichTextNode) -> HtmlElement<html::AnyElement> {
// for handler in NODE_HANDLERS.iter() {
// let node_type = match node.clone() {
// // RichTextNode::Block{node_type, data: _, content: _} => node_type,
// // RichTextNode::Text{node_type, data: _, value: _, marks: _} => node_type,
// RichTextNode::Block{node_type, content: _} => node_type,
// RichTextNode::Text{node_type, value: _} => node_type,
// };
// if handler.node_type == node_type {
// // return (handler.function)(node)
// }
// }
// empty_handler()
// }
// fn _render_children(children: &Vec<RichTextNode>) -> Vec<HtmlElement<html::AnyElement>> {
// children.iter().map(|child| _node_dispatcher(child)).collect()
// }

View file

@ -1,4 +1,4 @@
$mainGreen: darken(#2E7D32, 10); $mainGreen: #2b762f;
$mainGrey: #0F0F0F; $mainGrey: #0F0F0F;
@mixin top-level-padding { @mixin top-level-padding {
@ -30,6 +30,7 @@ body {
text-align: left; text-align: left;
font-weight: 300; font-weight: 300;
font-size: 16px; font-size: 16px;
line-height: 1.4;
background: white; background: white;
color: $mainGrey; color: $mainGrey;
@ -50,6 +51,7 @@ body {
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: "Roboto Slab", serif; font-family: "Roboto Slab", serif;
font-weight: 300; font-weight: 300;
line-height: 1.3;
} }
h1 { font-size: 42px; } h1 { font-size: 42px; }
h2 { font-size: 32px; } h2 { font-size: 32px; }
@ -91,7 +93,7 @@ body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
gap: 16px; gap: 16px;
h1 { h1 {
@ -104,9 +106,11 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 8px;
a { a {
color: white!important; color: white!important;
font-size: 22px;
} }
} }
} }
@ -191,5 +195,62 @@ body {
} }
} }
} }
}
.gallery-wrapper {
max-width: 1232px + 32px!important;
.open-entry {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 32px;
img {
object-fit: contain;
width: 100%;
max-height: 90vh;
max-width: 1232px;
align-self: center;
}
h3 {
margin-bottom: 8px;
}
p {
margin-bottom: 64px;
}
}
.gallery {
h3 {
width: fit-content;
}
ul {
width: fit-content;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
padding: 0;
list-style: none;
gap: 16px;
li {
img {
height: 400px;
width: 400px;
object-fit: contain;
@media all and (max-width: 1000px) {
height: 300px;
width: 300px;
}
}
}
}
}
} }