Add Gallery + WIP blog
This commit is contained in:
parent
f1c4111e1d
commit
68ca7284fb
22 changed files with 1725 additions and 75 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
.env
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
|
898
Cargo.lock
generated
898
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -16,11 +16,17 @@ leptos_router = { version = "0.6", features = ["nightly"] }
|
|||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "=0.2.93"
|
||||
wasm-bindgen = "=0.2.95"
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
http = "1"
|
||||
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]
|
||||
hydrate = [
|
||||
|
@ -44,7 +50,8 @@ ssr = [
|
|||
|
||||
[package.metadata.leptos-i18n]
|
||||
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
|
||||
[profile.wasm-release]
|
||||
|
|
|
@ -2,5 +2,11 @@ home: Home
|
|||
available_in: "Available in:"
|
||||
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
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
home: Hejmo
|
||||
available_in: "Havebla en:"
|
||||
partial_translations:
|
||||
partial_translations: (malkompletaj tradukoj)
|
||||
|
||||
blog: Blogo
|
||||
welcome_blog: Bonvenon al mia blogo!
|
||||
|
||||
gallery: Galerio
|
||||
|
||||
tanguy_gerome:
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
home: Kodu
|
||||
available_in: "Saadaval keeles:"
|
||||
partial_translations:
|
||||
partial_translations: (puudulikud tõlked)
|
||||
|
||||
blog: Blogi
|
||||
welcome_blog: Tere tulemast minu blogisse!
|
||||
|
||||
gallery: Galerii
|
||||
|
||||
tanguy_gerome:
|
||||
|
|
|
@ -2,5 +2,9 @@ home: Koti
|
|||
available_in: "Saatavilla kielillä:"
|
||||
partial_translations: (keskeneräisiä käännöksiä)
|
||||
|
||||
blog: Blogi
|
||||
welcome_blog: Tervetuloa blogiini!
|
||||
|
||||
gallery: Galleria
|
||||
|
||||
tanguy_gerome:
|
||||
|
|
|
@ -2,5 +2,9 @@ home: Accueil
|
|||
available_in: "Disponible en:"
|
||||
partial_translations: (traductions partielles)
|
||||
|
||||
blog: Blog
|
||||
welcome_blog: Bienvenue sur mon blog!
|
||||
|
||||
gallery: Gallerie
|
||||
|
||||
tanguy_gerome:
|
||||
|
|
|
@ -2,5 +2,9 @@ home: zdani
|
|||
available_in: "fanva fi "
|
||||
partial_translations: (nalmu'o puvyfanva)
|
||||
|
||||
blog: snukarni
|
||||
welcome_blog: .ui do zanvi'e fi le mi snukarni
|
||||
|
||||
gallery: larmuzga
|
||||
|
||||
tanguy_gerome: .tangis.jerom.
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
home: Hem
|
||||
available_in: "Finns på:"
|
||||
partial_translations:
|
||||
partial_translations: (saknade översättningar)
|
||||
|
||||
blog: Blogg
|
||||
welcome_blog: Välkommen till min blogg!
|
||||
|
||||
gallery: Galleri
|
||||
|
||||
tanguy_gerome:
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
buildImage = 'ghcr.io/railwayapp/nixpacks:debian-1730765032'
|
||||
|
||||
[phases.setup]
|
||||
# aptPkgs = ['libc6']
|
||||
|
||||
[phases.install]
|
||||
dependsOn = ['setup']
|
||||
# nixPkgs = ['cargo-leptos']
|
||||
# cmds = ['cargo install -f wasm-bindgen-cli --version 0.2.93']
|
||||
cmds = ['cargo install cargo-quickinstall', 'cargo quickinstall cargo-leptos']
|
||||
# cmds = ['cargo install cargo-leptos']
|
||||
# cmds = ['rustup target add wasm32-unknown-unknown']
|
||||
|
||||
[phases.build]
|
||||
|
|
69
src/app.rs
69
src/app.rs
|
@ -1,10 +1,13 @@
|
|||
leptos_i18n::load_locales!();
|
||||
use i18n::*;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::i18n::*;
|
||||
use crate::error_template::{AppError, ErrorTemplate};
|
||||
// use crate::services::contentful::get_rich_text_page;
|
||||
// use crate::services::rich_text;
|
||||
|
||||
use crate::routes::gallery;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
|
@ -34,6 +37,12 @@ pub fn App() -> impl IntoView {
|
|||
<Routes>
|
||||
<I18nRoute view=|| view! { <Outlet/> }>
|
||||
<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>
|
||||
</Routes>
|
||||
<Footer/>
|
||||
|
@ -50,19 +59,19 @@ pub fn LanguageSwitcher() -> impl IntoView {
|
|||
<div class="language-switcher main-width">
|
||||
<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::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::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::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::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::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::eo)>esperanto</button>
|
||||
// <button class="link" on:click=move |_| i18n.set_locale(Locale::jbo)>lojban</button>
|
||||
<p>{t!(i18n, partial_translations)}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Header() -> impl IntoView {
|
||||
pub fn Header() -> impl IntoView {
|
||||
let i18n = use_i18n();
|
||||
|
||||
view! {
|
||||
|
@ -72,6 +81,8 @@ fn Header() -> impl IntoView {
|
|||
<h1><span>tanguy</span><span>.gerome</span><span>.fi</span></h1>
|
||||
<div class="links">
|
||||
<A href="/">{t!(i18n, home)}</A>
|
||||
// <A href="/blog">{t!(i18n, blog)}</A>
|
||||
<A href="/gallery">{t!(i18n, gallery)}</A>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
|
@ -81,13 +92,13 @@ fn Header() -> impl IntoView {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<LanguageSwitcher/>
|
||||
// <LanguageSwitcher/>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Footer() -> impl IntoView {
|
||||
pub fn Footer() -> impl IntoView {
|
||||
let i18n = use_i18n();
|
||||
|
||||
view! {
|
||||
|
@ -105,11 +116,41 @@ fn Footer() -> impl IntoView {
|
|||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<main class="main-width">
|
||||
<h1>"Welcome!"</h1>
|
||||
<p>"This website is under construction, please check back later :)"</p>
|
||||
<h1>Welcome!</h1>
|
||||
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
|
103
src/blog.rs
Normal file
103
src/blog.rs
Normal 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
0
src/components/mod.rs
Normal file
|
@ -1,5 +1,12 @@
|
|||
leptos_i18n::load_locales!();
|
||||
pub mod app;
|
||||
pub mod blog;
|
||||
pub mod error_template;
|
||||
|
||||
pub mod components;
|
||||
pub mod routes;
|
||||
pub mod services;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fileserv;
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if cfg!(debug_assertions) {
|
||||
let _ = dotenvy::dotenv();
|
||||
}
|
||||
use axum::Router;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
|
128
src/routes/gallery.rs
Normal file
128
src/routes/gallery.rs
Normal 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
2
src/routes/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
pub mod gallery;
|
268
src/services/contentful.rs
Normal file
268
src/services/contentful.rs
Normal 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
2
src/services/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod contentful;
|
||||
pub mod rich_text;
|
193
src/services/rich_text.rs
Normal file
193
src/services/rich_text.rs
Normal 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()
|
||||
// }
|
|
@ -1,4 +1,4 @@
|
|||
$mainGreen: darken(#2E7D32, 10);
|
||||
$mainGreen: #2b762f;
|
||||
$mainGrey: #0F0F0F;
|
||||
|
||||
@mixin top-level-padding {
|
||||
|
@ -30,6 +30,7 @@ body {
|
|||
text-align: left;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
|
||||
background: white;
|
||||
color: $mainGrey;
|
||||
|
@ -50,6 +51,7 @@ body {
|
|||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Roboto Slab", serif;
|
||||
font-weight: 300;
|
||||
line-height: 1.3;
|
||||
}
|
||||
h1 { font-size: 42px; }
|
||||
h2 { font-size: 32px; }
|
||||
|
@ -91,7 +93,7 @@ body {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
|
||||
h1 {
|
||||
|
@ -104,9 +106,11 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
a {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue