diff --git a/Cargo.lock b/Cargo.lock index 3bc41c0..d129790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,6 +1797,7 @@ version = "0.3.0" dependencies = [ "actix-files", "actix-web", + "anyhow", "cfg-if", "console_error_panic_hook", "console_log", @@ -1813,6 +1814,7 @@ dependencies = [ "serde", "serde_yaml", "simple_logger", + "thiserror", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 8a4070f..4bd2ef8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] leptos = { version = "0.5", features = ["nightly"] } -leptos_actix = { version = "0.5", optional = true } leptos_meta = { version = "0.5", features = ["nightly"] } leptos_router = { version = "0.5", features = ["nightly"] } gloo-net = { version = "0.4", features = ["http"] } @@ -36,11 +35,14 @@ console_error_panic_hook = { version = "0.1", optional = true } # dependecies for server (enable when ssr set) actix-files = { version = "0.6", optional = true } actix-web = { version = "4", features = ["macros"], optional = true } +leptos_actix = { version = "0.5", optional = true } futures = { version = "0.3", optional = true } simple_logger = { version = "4.2", optional = true } pulldown-cmark = { version = "0.9", optional = true } # Markdown parser gray_matter = { version = "0.2", optional = true } # frontmatter parser serde_yaml = { version = "0.9", optional = true } +anyhow = { version = "1.0", optional = true } +thiserror = { version = "1.0", optional = true } [features] default = ["csr"] @@ -71,7 +73,9 @@ ssr = [ "dep:simple_logger", "dep:pulldown-cmark", "dep:gray_matter", - "dep:serde_yaml" + "dep:serde_yaml", + "dep:anyhow", + "dep:thiserror" ] [package.metadata.cargo-all-features] diff --git a/data_src/posts/test.md b/data_src/posts/test.md index fea2c17..0cc74e5 100644 --- a/data_src/posts/test.md +++ b/data_src/posts/test.md @@ -1,10 +1,13 @@ --- image_path: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Test-Logo.svg/783px-Test-Logo.svg.png?20150906031702" +slug: test_layout title: Testing layout date: 2023-11-26 description: Testing the layout of the site. project_link: none draft: true +tags: + - test --- # Heading 1 diff --git a/data_src/tags.yml b/data_src/tags.yml deleted file mode 100644 index d5fb5bf..0000000 --- a/data_src/tags.yml +++ /dev/null @@ -1,168 +0,0 @@ -tags: - rust: - name: Rust - url: https://www.rust-lang.org/ - groups: - - lang - - backend - - system - - web - java: - name: Java - url: https://www.java.com/fr/ - groups: - - lang - - backend - - desktop - cpp: - name: C++ - url: https://isocpp.org/ - groups: - - lang - - backend - - system - - game - react: - name: React - url: https://fr.legacy.reactjs.org/ - groups: - - frontend - symfony: - name: Symfony - url: https://symfony.com/ - groups: - - backend - - web - flutter: - name: Flutter - url: https://flutter.dev/ - groups: - - mobile - - desktop - ruby_on_rails: - name: Ruby on rails - url: https://rubyonrails.org/ - groups: - - backend - - frontend - - web - hotwired: - name: Hotwired - url: https://hotwired.dev/ - groups: - - frontend - - web - docker: - name: Docker - url: https://www.docker.com/ - groups: - - devops - steam_api: - name: Steam API - url: https://partner.steamgames.com/doc/sdk/api/example - groups: - - game - gitlab_ci: - name: Gitlab CI - url: https://docs.gitlab.com/ee/ci/ - groups: - - devops - unity: - name: Unity 3D - url: https://unity.com/fr - groups: - - game - wordpress: - name: Wordpress - url: https://wordpress.com/fr/ - groups: - - web - cordova: - name: Cordova - url: https://cordova.apache.org/ - groups: - - mobile - electron: - name: Electron - url: https://www.electronjs.org/ - groups: - - desktop - lwjgl: - name: LWJGL - url: https://www.lwjgl.org/ - groups: - - desktop - - game - opengl: - name: OpenGL - url: https://www.opengl.org/ - groups: - - desktop - - game - vulkan: - name: Vulkan - url: https://www.vulkan.org/ - groups: - - desktop - - game - midi: - name: MIDI - url: https://fr.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - groups: - - desktop - - web - - music - - game - requirejs: - name: RequireJS - url: https://requirejs.org/ - groups: - - frontend - - web - webpack: - name: Webpack - url: https://webpack.js.org/ - groups: - - frontend - - web - vite: - name: Vite - url: https://vitejs.dev/ - groups: - - frontend - - web - maven: - name: Maven - url: https://maven.apache.org/ - groups: - - desktop - gradle: - name: Gradle - url: https://gradle.org/ - groups: - - desktop - - mobile - babylonjs: - name: BabylonJS - url: https://www.babylonjs.com/ - groups: - - game - rocket_rs: - name: Rocket.rs - url: https://rocket.rs/ - groups: - - backend - - web - actix_web: - name: Actix Web - url: https://actix.rs/ - groups: - - backend - - web - leptos: - name: Leptos - url: https://leptos.dev/ - groups: - - web - - backend - - frontend \ No newline at end of file diff --git a/src/app/components/mon_parcours.rs b/src/app/components/mon_parcours.rs index d07587b..427db64 100644 --- a/src/app/components/mon_parcours.rs +++ b/src/app/components/mon_parcours.rs @@ -62,7 +62,7 @@ const GRADLE_TAG : Lang<'static> = Lang { lang: "Gradle", url: "https://gradle.o const BABYLONJS_TAG : Lang<'static> = Lang { lang: "BabylonJS", url: "https://www.babylonjs.com/" }; const ROCKET_RS_TAG : Lang<'static> = Lang { lang: "Rocket", url: "https://rocket.rs/" }; const ACTIX_WEB_TAG : Lang<'static> = Lang { lang: "Actix Web", url: "https://actix.rs/" }; -const LEPTOS_TAG : Lang<'static> = Lang { lang: "Leptos", url: "https://leptos.dev/" }; +const _LEPTOS_TAG : Lang<'static> = Lang { lang: "Leptos", url: "https://leptos.dev/" }; #[component] diff --git a/src/app/mod.rs b/src/app/mod.rs index 1d304ce..87881e6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,12 @@ pub mod components; + pub mod models; + mod pages; +#[cfg(feature = "ssr")] +mod utils; + use leptos::*; use leptos_meta::*; use leptos_router::*; diff --git a/src/app/models/mod.rs b/src/app/models/mod.rs index 69d39bb..9375f3d 100644 --- a/src/app/models/mod.rs +++ b/src/app/models/mod.rs @@ -2,6 +2,46 @@ mod post; pub use post::Post; -mod tag; +cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] { + use std::collections::HashMap; + use std::sync::Arc; -pub use tag::{Tags, Tag}; + pub struct Data { + pub posts: Vec>, + pub posts_by_slug: HashMap>, + pub posts_by_tag: HashMap>>, + } + + impl Data { + #[allow(dead_code)] // Use in main.rs + pub fn new() -> anyhow::Result { + let posts = crate::app::utils::data_src::get_all::("posts")? + .into_iter() + .map(|post| Arc::new(post)) + .collect::>(); + + let mut posts_by_slug : HashMap> = HashMap::new(); + let mut posts_by_tag : HashMap>> = HashMap::new(); + + for post in &posts { + posts_by_slug.insert(post.metadata.slug.clone(), post.clone()); + + for tag in &post.metadata.tags { + let tag = tag.to_lowercase(); + let posts = posts_by_tag.entry(tag).or_insert(Vec::new()); + posts.push(post.clone()); + } + } + + Ok(Self { + posts, + posts_by_slug, + posts_by_tag, + }) + } + } + + + } +} \ No newline at end of file diff --git a/src/app/models/post.rs b/src/app/models/post.rs index 39a6e88..04ae40e 100644 --- a/src/app/models/post.rs +++ b/src/app/models/post.rs @@ -1,34 +1,43 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct PostMetadata { + pub slug: String, pub image_path: String, pub title: String, pub date: String, pub description: String, pub project_link: String, - pub draft: bool + pub draft: bool, + pub tags: Vec, } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Post { - pub slug : String, pub metadata: PostMetadata, pub content: String, } cfg_if::cfg_if! { if #[cfg(feature = "ssr")] { - impl Post { - fn from_content(content: String) -> Option { + #[derive(Debug, thiserror::Error)] + pub enum PostDeserializationError { + #[error("Invalid front matter")] + InvalidFrontMatter, + #[error("Invalid markdown")] + InvalidMarkdown, + } + + impl TryFrom for Post { + type Error = PostDeserializationError; + + fn try_from(content: String) -> Result { use gray_matter::{Matter, engine::YAML}; let matter = Matter::::new(); - let post_data = matter - .parse_with_struct::(&content)?; + .parse_with_struct::(&content) + .ok_or_else(|| PostDeserializationError::InvalidFrontMatter)?; - let metadata = post_data.data; - let slug = format!("{}_{}", metadata.date, metadata.title.to_lowercase().replace(' ', "_")); let content = post_data.content; use pulldown_cmark::{Parser, Options, html}; @@ -36,49 +45,14 @@ cfg_if::cfg_if! { let mut html_output = String::new(); html::push_html(&mut html_output, parser); - Some(Self { - slug, - metadata, - content: html_output, - }) - } - - fn from_path(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path); - let content = match content { - Ok(content) => content, - Err(e) => { - eprintln!("{:?}", e); - return None; - } - }; - - Self::from_content(content) - } - - pub fn get_all(folder: &str) -> Result, String> { - use std::{path::Path, fs::read_dir}; - - let mut posts: Vec = Vec::new(); - - let folder_path = Path::new("data_src").join(folder); - let paths = read_dir(folder_path).map_err(|e| e.to_string())?; - - for path_result in paths { - let path = match path_result { - Ok(path) => path.path(), - Err(e) => { - eprintln!("{:?}", e); - continue; - } - }; - - if let Some(post) = Self::from_path(&path) { - posts.push(post); - } + if html_output.is_empty() { + return Err(PostDeserializationError::InvalidMarkdown); } - Ok(posts) + Ok(Self { + metadata: post_data.data, + content: html_output, + }) } } } diff --git a/src/app/models/tag.rs b/src/app/models/tag.rs deleted file mode 100644 index 403afdc..0000000 --- a/src/app/models/tag.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Tag { - pub name : String, - pub url : String, - pub groups : Vec, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Tags { - pub tags : HashMap, -} - -cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] { - impl Tags { - fn from_content(content: String) -> Option { - let tags = serde_yaml::from_str(&content).ok()?; - - Some(Self { - tags - }) - } - - fn from_path(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path); - let content = match content { - Ok(content) => content, - Err(e) => { - eprintln!("{:?}", e); - return None; - } - }; - - Self::from_content(content) - } - - fn from_default() -> Option { - let path = std::path::Path::new("data_src").join("tags.yml"); - Self::from_path(&path) - } - } - } -} diff --git a/src/app/pages/posts.rs b/src/app/pages/posts.rs index 453f178..96c2242 100644 --- a/src/app/pages/posts.rs +++ b/src/app/pages/posts.rs @@ -10,23 +10,38 @@ use crate::app::{ #[server] pub async fn get_posts() -> Result, ServerFnError> { - let posts = Post::get_all("posts") - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; - - Ok(posts) + leptos_actix::extract( + |data: actix_web::web::Data| async move { + let data = data.into_inner(); + data.posts + .iter() + .map(|post| { + Post { + metadata: post.metadata.clone(), + content: post.content.clone(), + } + }) + .collect::, >() + }, + ).await } #[server] pub async fn get_post( slug: String ) -> Result { - let posts = Post::get_all("posts") - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; - - let post = posts.into_iter().find(|post| post.slug == slug) - .ok_or(ServerFnError::ServerError("Post not found".to_string()))?; - - Ok(post) + leptos_actix::extract( + |data: actix_web::web::Data| async move { + let data = data.into_inner(); + data.posts_by_slug.get(&slug) + .and_then(|post| Some(Post { + metadata: post.metadata.clone(), + content: post.content.clone(), + })) + }, + ) + .await + .and_then(|post| post.ok_or_else(|| ServerFnError::ServerError("Post not found".to_string()))) } #[component] @@ -37,7 +52,7 @@ pub fn PostList() -> impl IntoView { posts.and_then(|posts| { posts.iter() .map(|post| view! { - + format!("Image { diff --git a/src/app/utils/data_src.rs b/src/app/utils/data_src.rs new file mode 100644 index 0000000..09fff16 --- /dev/null +++ b/src/app/utils/data_src.rs @@ -0,0 +1,33 @@ +#[allow(dead_code)] // Use in main.rs +pub fn get_all(folder: &str) -> anyhow::Result> +where + T: TryFrom, +{ + use std::{path::Path, fs::read_dir}; + + let mut datas: Vec = Vec::new(); + + let folder_path = Path::new("data_src").join(folder); + let paths = read_dir(folder_path)?; + + for path_result in paths { + let data : Result = path_result + .and_then(|path| std::fs::File::open(path.path())) + .map_err(|e| format!("Open file Error: {:?}", e)) + .and_then(|mut file| { + use std::io::Read; + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| format!("Read file Error: {:?}", e))?; + T::try_from(content) + .map_err(|_e| "Parse file Error".to_string()) + }); + + match data { + Ok(data) => datas.push(data), + Err(e) => eprintln!("{:?}", e), + } + } + + Ok(datas) +} \ No newline at end of file diff --git a/src/app/utils/mod.rs b/src/app/utils/mod.rs new file mode 100644 index 0000000..0dfbad9 --- /dev/null +++ b/src/app/utils/mod.rs @@ -0,0 +1 @@ +pub(crate) mod data_src; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 53782cf..a918395 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ cfg_if! { let site_root = &leptos_options.site_root; let routes = &routes; App::new() + .app_data(web::Data::new(app::models::Data::new().unwrap())) // Must panic if data can't be loaded .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { }) .service(Files::new("/", site_root))