Add resume, update leptos to 0.7, and use Directus instead of Contentful
This commit is contained in:
parent
cb70ffb3bb
commit
d2702ae39f
22 changed files with 1420 additions and 1976 deletions
3
.dockerignore
Executable file
3
.dockerignore
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
target
|
||||||
|
.git
|
2019
Cargo.lock
generated
2019
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
62
Cargo.toml
62
Cargo.toml
|
@ -7,47 +7,67 @@ edition = "2021"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7", optional = true }
|
leptos = { version = "0.7.0", features = ["nightly"] }
|
||||||
console_error_panic_hook = "0.1"
|
leptos_router = { version = "0.7.0", features = ["nightly"] }
|
||||||
leptos = { version = "0.6", features = ["nightly"] }
|
axum = { version = "0.7.0", optional = true }
|
||||||
leptos_axum = { version = "0.6", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true}
|
||||||
leptos_meta = { version = "0.6", features = ["nightly"] }
|
leptos_axum = { version = "0.7.0", optional = true }
|
||||||
leptos_router = { version = "0.6", features = ["nightly"] }
|
leptos_meta = { version = "0.7.0" }
|
||||||
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 }
|
wasm-bindgen = { version = "=0.2.100", optional = true }
|
||||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
# tower = { version = "0.4", features = ["util"], optional = true }
|
||||||
wasm-bindgen = "=0.2.100"
|
# tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
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.5.11", default-features = false, features = ["yaml_files", "track_locale_files"] }
|
||||||
serde = "1.0.213"
|
leptos_i18n_router = { version = "0.5.11" }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
serde = "1.0.213"
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
serde_path_to_error = "0.1.16"
|
# serde_path_to_error = "0.1.16"
|
||||||
reqwest = { version = "0.12.9", features = ["json"]}
|
reqwest = { version = "0.12.9", features = ["json"]}
|
||||||
contentful = "0.8.0"
|
markdown = "1.0.0"
|
||||||
|
# contentful = "0.8.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = [
|
hydrate = [
|
||||||
"leptos/hydrate",
|
"leptos/hydrate",
|
||||||
"leptos_meta/hydrate",
|
"dep:console_error_panic_hook",
|
||||||
"leptos_router/hydrate",
|
"dep:wasm-bindgen",
|
||||||
"leptos_i18n/hydrate",
|
"leptos_i18n/hydrate",
|
||||||
]
|
]
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
"dep:tower",
|
|
||||||
"dep:tower-http",
|
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
"dep:tracing",
|
"leptos_i18n/ssr",
|
||||||
"leptos_i18n/axum",
|
"leptos_i18n/axum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# [features]
|
||||||
|
# hydrate = [
|
||||||
|
# "leptos/hydrate",
|
||||||
|
# "leptos_meta/hydrate",
|
||||||
|
# "leptos_router/hydrate",
|
||||||
|
# "leptos_i18n/hydrate",
|
||||||
|
# ]
|
||||||
|
# ssr = [
|
||||||
|
# "dep:axum",
|
||||||
|
# "dep:tokio",
|
||||||
|
# "dep:tower",
|
||||||
|
# "dep:tower-http",
|
||||||
|
# "dep:leptos_axum",
|
||||||
|
# "leptos/ssr",
|
||||||
|
# "leptos_meta/ssr",
|
||||||
|
# "leptos_router/ssr",
|
||||||
|
# "dep:tracing",
|
||||||
|
# "leptos_i18n/axum",
|
||||||
|
# ]
|
||||||
|
|
||||||
[package.metadata.leptos-i18n]
|
[package.metadata.leptos-i18n]
|
||||||
default = "en"
|
default = "en"
|
||||||
# locales = ["en", "fr", "fi", "sv", "eo", "uk", "et", "jbo"]
|
# locales = ["en", "fr", "fi", "sv", "eo", "uk", "et", "jbo"]
|
||||||
|
|
86
Dockerfile
Executable file
86
Dockerfile
Executable file
|
@ -0,0 +1,86 @@
|
||||||
|
# Get started with a build env with Rust nightly
|
||||||
|
FROM rustlang/rust:nightly-alpine as builder
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache bash curl npm libc-dev binaryen openssl openssl-dev ca-certificates
|
||||||
|
|
||||||
|
RUN npm install -g sass
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.3 -LsSf https://github.com/leptos-rs/cargo-leptos/releases/latest/download/cargo-leptos-installer.sh | sh
|
||||||
|
|
||||||
|
# Add the WASM target
|
||||||
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
ENV RUSTUP_PERMIT_COPY_RENAME=true
|
||||||
|
RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo leptos build --release -vv
|
||||||
|
|
||||||
|
FROM rustlang/rust:nightly-alpine as runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /work/target/release/tanguy-gerome-fi /app/
|
||||||
|
COPY --from=builder /work/target/site /app/site
|
||||||
|
COPY --from=builder /work/Cargo.toml /app/
|
||||||
|
|
||||||
|
# # Get started with a build env with Rust nightly
|
||||||
|
# FROM rustlang/rust:nightly-bullseye AS builder
|
||||||
|
|
||||||
|
# # If you’re using stable, use this instead
|
||||||
|
# # FROM rust:1.74-bullseye as builder
|
||||||
|
|
||||||
|
# # Install cargo-binstall, which makes it easier to install other
|
||||||
|
# # cargo extensions like cargo-leptos
|
||||||
|
# RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||||
|
# RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||||
|
# RUN cp cargo-binstall /usr/local/cargo/bin
|
||||||
|
|
||||||
|
# # Install required tools
|
||||||
|
# RUN apt-get update -y \
|
||||||
|
# && apt-get install -y --no-install-recommends clang
|
||||||
|
|
||||||
|
# # Install cargo-leptos
|
||||||
|
# RUN cargo binstall cargo-leptos -y
|
||||||
|
|
||||||
|
# # Add the WASM target
|
||||||
|
# RUN rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
|
# # Make an /app dir, which everything will eventually live in
|
||||||
|
# RUN mkdir -p /app
|
||||||
|
# WORKDIR /app
|
||||||
|
# COPY . .
|
||||||
|
|
||||||
|
# # Build the app
|
||||||
|
# RUN cargo leptos build --release -vv
|
||||||
|
|
||||||
|
# FROM debian:bullseye-slim AS runtime
|
||||||
|
# WORKDIR /app
|
||||||
|
# RUN apt-get update -y \
|
||||||
|
# && apt-get install -y --no-install-recommends openssl libssl-dev ca-certificates \
|
||||||
|
# && apt-get autoremove -y \
|
||||||
|
# && apt-get clean -y \
|
||||||
|
# && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# # -- NB: update binary name from "leptos_start" to match your app name in Cargo.toml --
|
||||||
|
# # Copy the server binary to the /app directory
|
||||||
|
# COPY --from=builder /app/target/release/tanguy-gerome-fi /app/
|
||||||
|
|
||||||
|
# # /target/site contains our JS/WASM/CSS, etc.
|
||||||
|
# COPY --from=builder /app/target/site /app/site
|
||||||
|
|
||||||
|
# # Copy Cargo.toml if it’s needed at runtime
|
||||||
|
# COPY --from=builder /app/Cargo.toml /app/
|
||||||
|
|
||||||
|
# Set any required env variables and
|
||||||
|
ENV RUST_LOG="info"
|
||||||
|
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||||
|
ENV LEPTOS_SITE_ROOT="site"
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# -- NB: update binary name from "leptos_start" to match your app name in Cargo.toml --
|
||||||
|
# Run the server
|
||||||
|
CMD ["/app/tanguy-gerome-fi"]
|
||||||
|
|
|
@ -2,6 +2,8 @@ home: Home
|
||||||
available_in: "Available in:"
|
available_in: "Available in:"
|
||||||
partial_translations: (partial translations)
|
partial_translations: (partial translations)
|
||||||
|
|
||||||
|
resume: Resume
|
||||||
|
|
||||||
blog: Blog
|
blog: Blog
|
||||||
welcome_blog: Welcome to my blog!
|
welcome_blog: Welcome to my blog!
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ home: Koti
|
||||||
available_in: "Saatavilla kielillä:"
|
available_in: "Saatavilla kielillä:"
|
||||||
partial_translations: (keskeneräisiä käännöksiä)
|
partial_translations: (keskeneräisiä käännöksiä)
|
||||||
|
|
||||||
|
resume: Ansioluettelo
|
||||||
|
|
||||||
blog: Blogi
|
blog: Blogi
|
||||||
welcome_blog: Tervetuloa blogiini!
|
welcome_blog: Tervetuloa blogiini!
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ home: Accueil
|
||||||
available_in: "Disponible en:"
|
available_in: "Disponible en:"
|
||||||
partial_translations: (traductions partielles)
|
partial_translations: (traductions partielles)
|
||||||
|
|
||||||
|
resume: CV
|
||||||
|
|
||||||
blog: Blog
|
blog: Blog
|
||||||
welcome_blog: Bienvenue sur mon blog!
|
welcome_blog: Bienvenue sur mon blog!
|
||||||
|
|
||||||
|
|
0
src/components/mod.rs → locales/uk.yaml
Normal file → Executable file
0
src/components/mod.rs → locales/uk.yaml
Normal file → Executable file
186
src/app.rs
186
src/app.rs
|
@ -1,13 +1,33 @@
|
||||||
use leptos::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||||
use leptos_router::*;
|
use leptos_router::{
|
||||||
|
components::{Outlet, ParentRoute, Route, Router, Routes, A}, path
|
||||||
|
};
|
||||||
|
use leptos_i18n_router::I18nRoute;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::i18n::*;
|
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;
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<AutoReload options=options.clone() />
|
||||||
|
<HydrationScripts options/>
|
||||||
|
<MetaTags/>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<App/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
|
@ -16,8 +36,6 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<I18nContextProvider>
|
<I18nContextProvider>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
|
||||||
<Stylesheet href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@100..900&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"/>
|
<Stylesheet href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@100..900&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"/>
|
||||||
|
|
||||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||||
|
@ -25,25 +43,18 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
<Title text="tanguy.gerome.fi"/>
|
<Title text="tanguy.gerome.fi"/>
|
||||||
|
|
||||||
<Router fallback=|| {
|
// content for this welcome page
|
||||||
let mut outside_errors = Errors::default();
|
<Router>
|
||||||
outside_errors.insert_with_default_key(AppError::NotFound);
|
|
||||||
view! {
|
|
||||||
<ErrorTemplate outside_errors/>
|
|
||||||
}
|
|
||||||
.into_view()
|
|
||||||
}>
|
|
||||||
<Header/>
|
<Header/>
|
||||||
<Routes>
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
<I18nRoute view=|| view! { <Outlet/> }>
|
<I18nRoute<Locale, _, _> view=Outlet>
|
||||||
<Route path="" view=HomePage/>
|
<Route path=path!("/") view=HomePage/>
|
||||||
// <Route path="/blog" view=BlogList/>
|
<Route path=path!("/resume") view=Resume/>
|
||||||
// <Route path="/blog/:slug" view=BlogPost/>
|
<ParentRoute path=path!("/gallery") view=crate::gallery::Gallery>
|
||||||
<Route path="/gallery" view=gallery::Gallery>
|
<Route path=path!(":slug") view=crate::gallery::GalleryEntry/>
|
||||||
<Route path=":slug" view=gallery::GalleryEntry/>
|
<Route path=path!("") view=|| view! {}/>
|
||||||
<Route path="" view=|| view! {}/>
|
</ParentRoute>
|
||||||
</Route>
|
</I18nRoute<Locale, _, _>>
|
||||||
</I18nRoute>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -81,17 +92,19 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<picture>
|
<picture>
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image"
|
||||||
src="https://images.ctfassets.net/e3magj9g6dp1/14q5L7K0BCol1gx0aCSCck/d1f69bfa404efed6a2dcc71401bbc16d/P5310039-1-2.jpg?w=1600&q=50&fm=avif"
|
src="https://directus.gerome.fi/assets/efdc0b69-7a48-4434-bf07-d406e42a971e?width=1600&fit=inside&format=auto&quality=50&withoutEnlargement=true"
|
||||||
alt="Banner"
|
alt="Banner"
|
||||||
/>
|
/>
|
||||||
<source srcset="https://images.ctfassets.net/e3magj9g6dp1/14q5L7K0BCol1gx0aCSCck/d1f69bfa404efed6a2dcc71401bbc16d/P5310039-1-2.jpg?w=1600&q=50&fm=jpg"/>
|
<source srcset="https://directus.gerome.fi/assets/efdc0b69-7a48-4434-bf07-d406e42a971e?width=1600&fit=inside&format=jpg&quality=50&withoutEnlargement=true"/>
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -110,7 +123,7 @@ pub fn Footer() -> impl IntoView {
|
||||||
<span class="name">{t!(i18n, tanguy_gerome)}</span>
|
<span class="name">{t!(i18n, tanguy_gerome)}</span>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://www.instagram.com/kapno.cc/" target="_blank" rel="noopener noreferrer">instagram @kapno.cc</a>
|
<a href="https://www.instagram.com/kapno.cc/" target="_blank" rel="noopener noreferrer">instagram @kapno.cc</a>
|
||||||
<a href="https://github.com/kapnoc" target="_blank" rel="noopener noreferrer">github @kapnoc</a>
|
<a href="https://forgejo.juustodiilerit.fi/tanguy" target="_blank" rel="noopener noreferrer">git (forgejo.juustodiilerit.fi)</a>
|
||||||
<a href="mailto:tanguy@gerome.fi" target="_blank" rel="noopener noreferrer">email tanguy@gerome.fi</a>
|
<a href="mailto:tanguy@gerome.fi" target="_blank" rel="noopener noreferrer">email tanguy@gerome.fi</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,44 +131,89 @@ pub fn Footer() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||||
|
pub struct HomePageTranslations {
|
||||||
|
content: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
pub struct HomePageSingleton {
|
||||||
|
translations: Option<HomePageTranslations>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(GetHomepage, "/api", "GetJson")]
|
||||||
|
pub async fn get_homepage(locale: String) -> Result<HomePageSingleton, ServerFnError> {
|
||||||
|
crate::services::directus::get_client()
|
||||||
|
.get_item::<HomePageSingleton>(&locale, "homepage", "homepage").await
|
||||||
|
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HomePage() -> impl IntoView {
|
pub fn HomePage() -> impl IntoView {
|
||||||
|
let i18n = use_i18n();
|
||||||
|
|
||||||
|
let homepage = Resource::new(move || i18n.get_locale(), move |locale| get_homepage(locale.to_string()));
|
||||||
view! {
|
view! {
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>Welcome!</h1>
|
<Suspense
|
||||||
|
fallback=move || view! { <div>"Loading..."</div> }
|
||||||
<p>I am <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>
|
>
|
||||||
|
{move || homepage.get()
|
||||||
<p>
|
.and_then(|homepage| homepage.ok())
|
||||||
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:
|
.and_then(|homepage| {
|
||||||
|
let html = markdown::to_html(&homepage.translations.unwrap_or_default().content);
|
||||||
<ul>
|
view! {
|
||||||
<li>Linux</li>
|
<div class="markdown" inner_html=html/>
|
||||||
<li>video games</li>
|
}.into()
|
||||||
<li>knitting</li>
|
})
|
||||||
<li>hiking</li>
|
}
|
||||||
<li>scouting</li>
|
</Suspense>
|
||||||
<li>language learning</li>
|
</main>
|
||||||
<li>photography (digital and film)</li>
|
}
|
||||||
</ul>
|
}
|
||||||
</p>
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||||
<p>I am also using this website as a way to practice in my language learning journey, so expect unfinished and low quality translations for anything other than English and French :D</p>
|
pub struct ResumeTranslations {
|
||||||
|
content: String
|
||||||
<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>
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
// {match page {
|
pub struct ResumeSingleton {
|
||||||
// Ok(page) => {
|
translations: Option<ResumeTranslations>
|
||||||
// view! {
|
}
|
||||||
// <div>
|
|
||||||
// {rich_text::document_handler(&page.english.rich_text_content)}
|
#[server(GetResume, "/api", "GetJson")]
|
||||||
// </div>
|
pub async fn get_resume(locale: String) -> Result<ResumeSingleton, ServerFnError> {
|
||||||
// }
|
crate::services::directus::get_client()
|
||||||
// },
|
.get_item::<ResumeSingleton>(&locale, "resume", "resume").await
|
||||||
// Err(error) => view! { <div><p>"Error: "{error.to_string()}</p></div> }
|
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||||
// }}
|
}
|
||||||
// </Await>
|
|
||||||
|
#[component]
|
||||||
|
pub fn Resume() -> impl IntoView {
|
||||||
|
let i18n = use_i18n();
|
||||||
|
|
||||||
|
let resume = Resource::new(move || i18n.get_locale(), move |locale| get_resume(locale.to_string()));
|
||||||
|
view! {
|
||||||
|
<main class="main-width">
|
||||||
|
<img
|
||||||
|
class="resume-image"
|
||||||
|
src="https://directus.gerome.fi/assets/0c33f439-4e1b-4a1f-a1ab-df1cc9b60f23?width=600&height=600&fit=cover&format=auto&quality=90&withoutEnlargement=true"
|
||||||
|
alt="Tanguy Gérôme - portait"
|
||||||
|
/>
|
||||||
|
<Suspense
|
||||||
|
fallback=move || view! { <div>"Loading..."</div> }
|
||||||
|
>
|
||||||
|
{move || resume.get()
|
||||||
|
.and_then(|resume| resume.ok())
|
||||||
|
.and_then(|resume| {
|
||||||
|
let html = markdown::to_html(&resume.translations.unwrap_or_default().content);
|
||||||
|
view! {
|
||||||
|
<div class="markdown" inner_html=html/>
|
||||||
|
}.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
103
src/blog.rs
103
src/blog.rs
|
@ -1,103 +0,0 @@
|
||||||
use leptos::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::i18n::*;
|
|
||||||
// use crate::services::contentful::get_markdown_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>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
use http::status::StatusCode;
|
|
||||||
use leptos::*;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Error)]
|
|
||||||
pub enum AppError {
|
|
||||||
#[error("Not Found")]
|
|
||||||
NotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppError {
|
|
||||||
pub fn status_code(&self) -> StatusCode {
|
|
||||||
match self {
|
|
||||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A basic function to display errors served by the error boundaries.
|
|
||||||
// Feel free to do more complicated things here than just displaying the error.
|
|
||||||
#[component]
|
|
||||||
pub fn ErrorTemplate(
|
|
||||||
#[prop(optional)] outside_errors: Option<Errors>,
|
|
||||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let errors = match outside_errors {
|
|
||||||
Some(e) => create_rw_signal(e),
|
|
||||||
None => match errors {
|
|
||||||
Some(e) => e,
|
|
||||||
None => panic!("No Errors found and we expected errors!"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Get Errors from Signal
|
|
||||||
let errors = errors.get_untracked();
|
|
||||||
|
|
||||||
// Downcast lets us take a type that implements `std::error::Error`
|
|
||||||
let errors: Vec<AppError> = errors
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
|
||||||
.collect();
|
|
||||||
println!("Errors: {errors:#?}");
|
|
||||||
|
|
||||||
// Only the response code for the first error is actually sent from the server
|
|
||||||
// this may be customized by the specific application
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
{
|
|
||||||
use leptos_axum::ResponseOptions;
|
|
||||||
let response = use_context::<ResponseOptions>();
|
|
||||||
if let Some(response) = response {
|
|
||||||
response.set_status(errors[0].status_code());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
|
||||||
<For
|
|
||||||
// a function that returns the items we're iterating over; a signal is fine
|
|
||||||
each= move || {errors.clone().into_iter().enumerate()}
|
|
||||||
// a unique key for each item as a reference
|
|
||||||
key=|(index, _error)| *index
|
|
||||||
// renders each item to a view
|
|
||||||
children=move |error| {
|
|
||||||
let error_string = error.1.to_string();
|
|
||||||
let error_code= error.1.status_code();
|
|
||||||
view! {
|
|
||||||
<h2>{error_code.to_string()}</h2>
|
|
||||||
<p>"Error: " {error_string}</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
use crate::app::App;
|
|
||||||
use axum::response::Response as AxumResponse;
|
|
||||||
use axum::{
|
|
||||||
body::Body,
|
|
||||||
extract::State,
|
|
||||||
http::{Request, Response, StatusCode},
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use leptos::*;
|
|
||||||
use tower::ServiceExt;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
|
|
||||||
pub async fn file_and_error_handler(
|
|
||||||
State(options): State<LeptosOptions>,
|
|
||||||
req: Request<Body>,
|
|
||||||
) -> AxumResponse {
|
|
||||||
let root = options.site_root.clone();
|
|
||||||
let (parts, body) = req.into_parts();
|
|
||||||
|
|
||||||
let mut static_parts = parts.clone();
|
|
||||||
static_parts.headers.clear();
|
|
||||||
if let Some(encodings) = parts.headers.get("accept-encoding") {
|
|
||||||
static_parts
|
|
||||||
.headers
|
|
||||||
.insert("accept-encoding", encodings.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if res.status() == StatusCode::OK {
|
|
||||||
res.into_response()
|
|
||||||
} else {
|
|
||||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
|
|
||||||
handler(Request::from_parts(parts, body))
|
|
||||||
.await
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_static_file(
|
|
||||||
request: Request<Body>,
|
|
||||||
root: &str,
|
|
||||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
||||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
|
||||||
// This path is relative to the cargo root
|
|
||||||
match ServeDir::new(root)
|
|
||||||
.precompressed_gzip()
|
|
||||||
.precompressed_br()
|
|
||||||
.oneshot(request)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(res) => Ok(res.into_response()),
|
|
||||||
Err(err) => Err((
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Error serving files: {err}"),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
134
src/gallery.rs
Normal file
134
src/gallery.rs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
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 ImageTranslations {
|
||||||
|
subtitle: Option<String>,
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Image {
|
||||||
|
slug: String,
|
||||||
|
title: String,
|
||||||
|
translations: Option<ImageTranslations>,
|
||||||
|
watermarked: Asset,
|
||||||
|
original: Asset,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Gallery {
|
||||||
|
images: Vec<Image>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(GetGallery, "/api", "GetJson")]
|
||||||
|
pub async fn get_gallery(locale: String) -> Result<Gallery, ServerFnError> {
|
||||||
|
let all_images = crate::services::directus::get_client().get_all_items_in_collection::<Image>(&locale, "image").await.unwrap_or(vec![]);
|
||||||
|
crate::services::directus::get_client().get_item_json("gallery", "gallery").await
|
||||||
|
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||||
|
.and_then(|gallery_json| {
|
||||||
|
let images_vec_json = gallery_json["data"]["images"].as_array().unwrap();
|
||||||
|
let images = images_vec_json.into_iter().map(|image_json| {
|
||||||
|
all_images.iter().find(|image| image.slug == image_json["item"])
|
||||||
|
.map(|found_image| found_image.clone())
|
||||||
|
});
|
||||||
|
let collected_images: Option<Vec<_>> = images.collect();
|
||||||
|
match collected_images {
|
||||||
|
Some(images) => Ok(Gallery{ images }),
|
||||||
|
_ => Err(ServerFnError::ServerError("aaa".to_string()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Gallery() -> impl IntoView {
|
||||||
|
let i18n = use_i18n();
|
||||||
|
|
||||||
|
let gallery = Resource::new(move || i18n.get_locale(), move |locale| get_gallery(locale.to_string()));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<main class="main-width gallery-wrapper">
|
||||||
|
<h2>{t!(i18n, gallery)}</h2>
|
||||||
|
<Outlet/>
|
||||||
|
<Suspense fallback=move || view! { <div>"Loading..."</div> }>
|
||||||
|
{move || gallery.get()
|
||||||
|
.and_then(|gallery| gallery.ok())
|
||||||
|
.and_then(|gallery| {
|
||||||
|
view! {
|
||||||
|
<div class="gallery-entries">
|
||||||
|
{
|
||||||
|
gallery.images.into_iter().map(|image| {
|
||||||
|
view! {
|
||||||
|
<a class="gallery-thumbnail" href={format!("/gallery/{}", image.slug)}>
|
||||||
|
<img src=format!("https://directus.gerome.fi/assets/{}?width=400&height=400&fit=inside&format=auto&quality=80&withoutEnlargement=true", image.original.id)/>
|
||||||
|
<p>{image.title}</p>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Params, PartialEq)]
|
||||||
|
struct GalleryEntryParams {
|
||||||
|
slug: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn GalleryEntry() -> impl IntoView {
|
||||||
|
let i18n = use_i18n();
|
||||||
|
let params = use_params::<GalleryEntryParams>();
|
||||||
|
let slug = move || {
|
||||||
|
params .read()
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|params| params.slug.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let gallery = Resource::new(move || i18n.get_locale(), move |locale| get_gallery(locale.to_string()));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<main class="main-width gallery-wrapper">
|
||||||
|
<Suspense fallback=move || view! { <div>"Loading..."</div> }>
|
||||||
|
{move || gallery.get()
|
||||||
|
.and_then(|gallery| gallery.ok())
|
||||||
|
.and_then(|gallery| gallery.images.iter()
|
||||||
|
.find(|image| image.slug == slug())
|
||||||
|
.map(|image| image.clone()))
|
||||||
|
.and_then(|image| {
|
||||||
|
let translations = image.translations.unwrap_or_default();
|
||||||
|
let subtitle = translations.subtitle.unwrap_or_default();
|
||||||
|
view! {
|
||||||
|
<div class="gallery-open-entry">
|
||||||
|
<img src=format!("https://directus.gerome.fi/assets/{}?width=1600&height=1600&fit=inside&format=auto&quality=90&withoutEnlargement=true", image.watermarked.id)/>
|
||||||
|
<h3>{image.title}</h3>
|
||||||
|
{if subtitle.len() > 0 {
|
||||||
|
Some(view!{<div><h4>{subtitle}</h4></div>})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}}
|
||||||
|
<p>{translations.description}</p>
|
||||||
|
</div>
|
||||||
|
}.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,19 +1,15 @@
|
||||||
leptos_i18n::load_locales!();
|
leptos_i18n::load_locales!();
|
||||||
pub mod app;
|
|
||||||
pub mod blog;
|
|
||||||
pub mod error_template;
|
|
||||||
|
|
||||||
pub mod components;
|
pub mod app;
|
||||||
pub mod routes;
|
|
||||||
pub mod services;
|
pub mod services;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
pub mod gallery;
|
||||||
pub mod fileserv;
|
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn hydrate() {
|
pub fn hydrate() {
|
||||||
use crate::app::*;
|
use crate::app::*;
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
leptos::mount_to_body(App);
|
leptos::mount::hydrate_body(App);
|
||||||
}
|
}
|
||||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -5,29 +6,29 @@ async fn main() {
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
}
|
}
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::*;
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use tanguy_gerome_fi::app::*;
|
use tanguy_gerome_fi::app::*;
|
||||||
use tanguy_gerome_fi::fileserv::file_and_error_handler;
|
|
||||||
|
|
||||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
let conf = get_configuration(None).unwrap();
|
||||||
// For deployment these variables are:
|
let addr = conf.leptos_options.site_addr;
|
||||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
|
||||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
|
||||||
// The file would need to be included with the executable when moved to deployment
|
|
||||||
let conf = get_configuration(None).await.unwrap();
|
|
||||||
let leptos_options = conf.leptos_options;
|
let leptos_options = conf.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
// Generate the list of routes in your Leptos App
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
// build our application with a route
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.leptos_routes(&leptos_options, routes, App)
|
.leptos_routes(&leptos_options, routes, {
|
||||||
.fallback(file_and_error_handler)
|
let leptos_options = leptos_options.clone();
|
||||||
|
move || shell(leptos_options.clone())
|
||||||
|
})
|
||||||
|
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||||
.with_state(leptos_options);
|
.with_state(leptos_options);
|
||||||
|
|
||||||
|
// run our app with hyper
|
||||||
|
// `axum::Server` is a re-export of `hyper::Server`
|
||||||
|
log!("listening on http://{}", &addr);
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
logging::log!("listening on http://{}", &addr);
|
|
||||||
axum::serve(listener, app.into_make_service())
|
axum::serve(listener, app.into_make_service())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -36,6 +37,6 @@ async fn main() {
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// no client-side main function
|
// no client-side main function
|
||||||
// unless we want this to work with e.g., Trunk for a purely client-side app
|
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||||
// see lib.rs for hydration function instead
|
// see lib.rs for hydration function instead
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
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);
|
|
||||||
let jpg_display_url = format!("https:{}?w=2400&h=2400&q=80&fm=jpg", &image.image_with_watermark.file.url);
|
|
||||||
view! {
|
|
||||||
<div class="open-entry">
|
|
||||||
<picture>
|
|
||||||
<img src=display_url alt=image.title.clone()/>
|
|
||||||
<source srcset=jpg_display_url/>
|
|
||||||
</picture>
|
|
||||||
<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 jpg_thumbnail_url = format!("https:{}?w=800&h=800&q=80&fm=jpg", &image.image_without_watermark.file.url);
|
|
||||||
let link_url = format!("/gallery/{}", &image.slug);
|
|
||||||
view! {
|
|
||||||
<li>
|
|
||||||
<A href=link_url>
|
|
||||||
<picture>
|
|
||||||
<img src=thumbnail_url alt=image.title/>
|
|
||||||
<source srcset=jpg_thumbnail_url/>
|
|
||||||
</picture>
|
|
||||||
</A>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}).collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(error) => view! { <div>"Error: "{error.to_string()}</div> }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}}
|
|
||||||
</Suspense>
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
pub mod gallery;
|
|
|
@ -1,51 +0,0 @@
|
||||||
use std::env;
|
|
||||||
use leptos::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct MarkdownPage {
|
|
||||||
pub title: String,
|
|
||||||
pub slug: String,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct MarkdownPageLanguagesWrapper {
|
|
||||||
pub internal_title: String,
|
|
||||||
pub slug: String,
|
|
||||||
pub english: MarkdownPage,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[server(GetRichTextPage, "/api", "GetJson")]
|
|
||||||
pub async fn get_markdown_page(slug: String) -> Result<MarkdownPageLanguagesWrapper, ServerFnError> {
|
|
||||||
let contentful_client = get_contentful_client();
|
|
||||||
let builder = contentful::QueryBuilder::new()
|
|
||||||
.content_type_is("markdownPageLanguagesWrapper")
|
|
||||||
.include(10)
|
|
||||||
.field_equals("fields.slug", &slug);
|
|
||||||
// let query_string = format!("?content_type=markdownPageLanguagesWrapper&include=10&fields.slug={}", slug);
|
|
||||||
// let pages = contentful_client.get_entries_by_query_string::<RichTextPageLanguagesWrapper>(&query_string).await;
|
|
||||||
let pages = contentful_client.get_entries::<MarkdownPageLanguagesWrapper>(Some(builder)).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_content = found_pages[0].english.content.clone();
|
|
||||||
dbg!(english_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);
|
|
||||||
}
|
|
84
src/services/directus.rs
Normal file
84
src/services/directus.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub struct DirectusClient {
|
||||||
|
access_token: String,
|
||||||
|
base_url: String,
|
||||||
|
reqwest_client: reqwest::Client
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Asset {
|
||||||
|
pub id: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectusClient {
|
||||||
|
pub fn new(access_token: &str, base_url: &str) -> DirectusClient {
|
||||||
|
let reqwest_client = reqwest::Client::new();
|
||||||
|
DirectusClient {
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
base_url: base_url.to_string(),
|
||||||
|
reqwest_client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_items_in_collection(&self, collection: &str, primary_key: &str) -> Result<Vec<String>> {
|
||||||
|
let response = self.reqwest_client.get(format!("{}/items/{}?fields={}", self.base_url, collection, primary_key))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.access_token))
|
||||||
|
.send().await?;
|
||||||
|
let items_json = response.json::<Vec<HashMap<String, String>>>().await?;
|
||||||
|
let ids = items_json.iter().map(|item| item[&primary_key.to_string()].clone()).collect();
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_item_json(&self, collection: &str, id: &str) -> Result<serde_json::Value> {
|
||||||
|
let response = self.reqwest_client.get(format!("{}/items/{}/{}?fields=*,*.*,translations.*", self.base_url, collection, id))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.access_token))
|
||||||
|
.send().await?;
|
||||||
|
let json = &response.json::<serde_json::Value>().await?;
|
||||||
|
Ok(json.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_item<T: for<'a> Deserialize<'a>>(&self, locale: &str, collection: &str, id: &str) -> Result<T> {
|
||||||
|
let response = self.reqwest_client.get(format!("{}/items/{}/{}?fields=*,*.*,translations.*", self.base_url, collection, id))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.access_token))
|
||||||
|
.send().await?;
|
||||||
|
let json = &response.json::<serde_json::Value>().await?;
|
||||||
|
let translations_json = json["data"]["translations"].clone();
|
||||||
|
let found_translation = match translations_json.as_array() {
|
||||||
|
Some(translations) => translations.iter().find(|translation| translation["languages_code"] == locale),
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
let mut new_json = json["data"].clone();
|
||||||
|
new_json["translations"] = found_translation.unwrap_or(&serde_json::Value::Null).clone();
|
||||||
|
let item = serde_json::from_value::<T>(new_json)?;
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_items_in_collection<T: for<'a> Deserialize<'a>>(&self, locale: &str, collection: &str) -> Result<Vec<T>> {
|
||||||
|
let response = self.reqwest_client.get(format!("{}/items/{}?fields=*,*.*,translations.*", self.base_url, collection))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.access_token))
|
||||||
|
.send().await?;
|
||||||
|
let json = &response.json::<serde_json::Value>().await?;
|
||||||
|
let items_json = json["data"].as_array().unwrap_or(&vec!()).clone();
|
||||||
|
let items = items_json.iter().map(|item_json| {
|
||||||
|
let translations_json = item_json["translations"].clone();
|
||||||
|
let found_translation = match translations_json.as_array() {
|
||||||
|
Some(translations) => translations.iter().find(|translation| translation["languages_code"] == locale),
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
let mut new_json = item_json.clone();
|
||||||
|
new_json["translations"] = found_translation.unwrap_or(&serde_json::Value::Null).clone();
|
||||||
|
let item = serde_json::from_value::<T>(new_json);
|
||||||
|
item.unwrap()
|
||||||
|
}).collect();
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client() -> DirectusClient {
|
||||||
|
let access_token = env::var("DIRECTUS_ACCESS_TOKEN").unwrap();
|
||||||
|
DirectusClient::new(&access_token, "https://directus.gerome.fi")
|
||||||
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod contentful;
|
|
||||||
pub mod rich_text;
|
pub mod directus;
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
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()
|
|
||||||
// }
|
|
148
style/main.scss
148
style/main.scss
|
@ -53,10 +53,10 @@ body {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
h1 { font-size: 42px; }
|
h1 { font-size: 42px; margin: 32px 0 16px 0; }
|
||||||
h2 { font-size: 32px; }
|
h2 { font-size: 32px; margin: 64px 0 16px 0; }
|
||||||
h3 { font-size: 22px; }
|
h3 { font-size: 22px; margin: 32px 0 16px 0; }
|
||||||
h4 { font-size: 18px; }
|
h4 { font-size: 18px; margin: 32px 0 16px 0; }
|
||||||
|
|
||||||
header {
|
header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -87,7 +87,7 @@ body {
|
||||||
.text {
|
.text {
|
||||||
@include top-level-padding;
|
@include top-level-padding;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
flex-shrink: 1;
|
flex-shrink: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -119,7 +119,11 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
picture {
|
picture {
|
||||||
display: contents;
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
height: 100%;
|
||||||
|
// display: contents;
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
@ -155,6 +159,16 @@ body {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
b, strong {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: $mainGrey;
|
background: $mainGrey;
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -204,67 +218,67 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-wrapper {
|
.resume-image {
|
||||||
max-width: 1232px + 32px!important;
|
float: right;
|
||||||
|
max-width: 300px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 16px;
|
||||||
|
|
||||||
.open-entry {
|
@media all and (max-width: 550px) {
|
||||||
display: flex;
|
float: unset;
|
||||||
flex-direction: column;
|
display: block;
|
||||||
align-items: flex-start;
|
margin: auto;
|
||||||
margin-top: 32px;
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
picture {
|
}
|
||||||
display: contents;
|
}
|
||||||
|
|
||||||
img {
|
.gallery-entries {
|
||||||
object-fit: contain;
|
width: fit-content;
|
||||||
width: 100%;
|
display: flex;
|
||||||
max-height: 90vh;
|
flex-direction: row;
|
||||||
max-width: 1232px;
|
flex-wrap: wrap;
|
||||||
align-self: center;
|
justify-content: center;
|
||||||
}
|
padding: 0;
|
||||||
}
|
gap: 16px;
|
||||||
|
}
|
||||||
h3 {
|
|
||||||
margin-bottom: 8px;
|
.gallery-thumbnail {
|
||||||
}
|
img {
|
||||||
|
height: 400px;
|
||||||
p {
|
width: 400px;
|
||||||
margin-bottom: 64px;
|
object-fit: contain;
|
||||||
}
|
|
||||||
}
|
@media all and (max-width: 1000px) {
|
||||||
|
height: 300px;
|
||||||
.gallery {
|
width: 300px;
|
||||||
h3 {
|
}
|
||||||
width: fit-content;
|
}
|
||||||
}
|
|
||||||
|
p {
|
||||||
ul {
|
text-align: center;
|
||||||
width: fit-content;
|
}
|
||||||
display: flex;
|
}
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
.gallery-open-entry {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
padding: 0;
|
flex-direction: column;
|
||||||
list-style: none;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
margin-top: 32px;
|
||||||
|
|
||||||
li {
|
img {
|
||||||
|
object-fit: contain;
|
||||||
picture {
|
width: 100%;
|
||||||
display: contents;
|
max-height: 90vh;
|
||||||
img {
|
max-width: 1232px;
|
||||||
height: 400px;
|
align-self: center;
|
||||||
width: 400px;
|
}
|
||||||
object-fit: contain;
|
|
||||||
|
h3 {
|
||||||
@media all and (max-width: 1000px) {
|
margin-bottom: 8px;
|
||||||
height: 300px;
|
}
|
||||||
width: 300px;
|
|
||||||
}
|
p {
|
||||||
}
|
margin-bottom: 64px;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue