Merge pull request 'Ajout tags' (#7) from refactor-tags into main
All checks were successful
deploy / docker (push) Successful in 13m27s
All checks were successful
deploy / docker (push) Successful in 13m27s
Reviewed-on: https://gitea.mrdev023.fr/florian.richer/portfolio/pulls/7
This commit is contained in:
commit
60a36f0eb8
17 changed files with 436 additions and 387 deletions
467
Cargo.lock
generated
467
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "portfolio"
|
name = "portfolio"
|
||||||
version = "0.2.3"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -10,10 +10,9 @@ crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "0.5", features = ["nightly"] }
|
leptos = { version = "0.5", features = ["nightly"] }
|
||||||
leptos_actix = { version = "0.5", optional = true }
|
|
||||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||||
gloo-net = { version = "0.4", features = ["http"] }
|
gloo-net = { version = "0.5", features = ["http"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
@ -30,16 +29,20 @@ leptos_icons = { version = "0.1.0", features = [
|
||||||
|
|
||||||
# dependecies for client (enable when csr or hydrate set)
|
# dependecies for client (enable when csr or hydrate set)
|
||||||
wasm-bindgen = { version = "0.2", optional = true }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
console_log = { version = "1", optional = true }
|
console_log = { version = "1.0", optional = true }
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|
||||||
# dependecies for server (enable when ssr set)
|
# dependecies for server (enable when ssr set)
|
||||||
actix-files = { version = "0.6", optional = true }
|
actix-files = { version = "0.6", optional = true }
|
||||||
actix-web = { version = "4", features = ["macros"], optional = true }
|
actix-web = { version = "4.4", features = ["macros"], optional = true }
|
||||||
|
leptos_actix = { version = "0.5", optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
simple_logger = { version = "4.2", optional = true }
|
simple_logger = { version = "4.3", optional = true }
|
||||||
pulldown-cmark = { version = "0.9", optional = true } # Markdown parser
|
pulldown-cmark = { version = "0.9", optional = true } # Markdown parser
|
||||||
gray_matter = { version = "0.2", optional = true } # frontmatter 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]
|
[features]
|
||||||
default = ["csr"]
|
default = ["csr"]
|
||||||
|
@ -70,6 +73,9 @@ ssr = [
|
||||||
"dep:simple_logger",
|
"dep:simple_logger",
|
||||||
"dep:pulldown-cmark",
|
"dep:pulldown-cmark",
|
||||||
"dep:gray_matter",
|
"dep:gray_matter",
|
||||||
|
"dep:serde_yaml",
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:thiserror"
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.cargo-all-features]
|
[package.metadata.cargo-all-features]
|
||||||
|
@ -94,7 +100,9 @@ opt-level = 'z'
|
||||||
name = "portfolio"
|
name = "portfolio"
|
||||||
site-root = "target/site"
|
site-root = "target/site"
|
||||||
site-pkg-dir = "pkg"
|
site-pkg-dir = "pkg"
|
||||||
|
style-file = "style/main.scss" # Important for Hot Reload to work properly (File must exist)
|
||||||
tailwind-input-file = "style/portfolio.css"
|
tailwind-input-file = "style/portfolio.css"
|
||||||
|
watch-additional-files = ["data_src"]
|
||||||
assets-dir = "public"
|
assets-dir = "public"
|
||||||
site-addr = "127.0.0.1:3000"
|
site-addr = "127.0.0.1:3000"
|
||||||
reload-port = 3001
|
reload-port = 3001
|
||||||
|
|
|
@ -33,11 +33,11 @@ COPY --from=builder /app/target/site /app/site
|
||||||
# Copy Cargo.toml if it’s needed at runtime
|
# Copy Cargo.toml if it’s needed at runtime
|
||||||
COPY --from=builder /app/Cargo.toml /app/
|
COPY --from=builder /app/Cargo.toml /app/
|
||||||
# Copy all data files
|
# Copy all data files
|
||||||
COPY --from=builder /app/markdowns /app/markdowns
|
COPY --from=builder /app/data_src /app/data_src
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Set any required env variables and
|
# Set any required env variables and
|
||||||
ENV RUST_LOG="info"
|
ENV RUST_LOG="warn"
|
||||||
ENV APP_ENVIRONMENT="production"
|
ENV APP_ENVIRONMENT="production"
|
||||||
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||||
ENV LEPTOS_SITE_ROOT="site"
|
ENV LEPTOS_SITE_ROOT="site"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
---
|
---
|
||||||
image_path: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Test-Logo.svg/783px-Test-Logo.svg.png?20150906031702"
|
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
|
title: Testing layout
|
||||||
date: 2023-11-26
|
date: 2023-11-26
|
||||||
description: Testing the layout of the site.
|
description: Testing the layout of the site.
|
||||||
project_link: none
|
project_link: none
|
||||||
draft: true
|
draft: true
|
||||||
|
tags:
|
||||||
|
- test
|
||||||
---
|
---
|
||||||
|
|
||||||
# Heading 1
|
# Heading 1
|
|
@ -31,4 +31,4 @@ mod title;
|
||||||
|
|
||||||
pub use title::Title;
|
pub use title::Title;
|
||||||
|
|
||||||
pub use mon_parcours::{MonParcours};
|
pub use mon_parcours::MonParcours;
|
||||||
|
|
|
@ -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 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 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 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]
|
#[component]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos_router::A;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Title(
|
pub fn Title(
|
||||||
|
@ -8,7 +9,7 @@ pub fn Title(
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<header>
|
<header>
|
||||||
<a href=href>r"< Retour"</a>
|
<A href=href>r"< Retour"</A>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
<span></span>
|
<span></span>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
mod pages;
|
mod pages;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
|
@ -1,3 +1,50 @@
|
||||||
mod post;
|
mod post;
|
||||||
|
|
||||||
pub use post::Post;
|
pub use post::Post;
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub posts: Vec<Arc<Post>>,
|
||||||
|
pub posts_by_slug: HashMap<String, Arc<Post>>,
|
||||||
|
pub posts_by_tag: HashMap<String, Vec<Arc<Post>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data {
|
||||||
|
#[allow(dead_code)] // Use in main.rs
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let posts = crate::app::utils::data_src::get_all::<Post>("posts")?
|
||||||
|
.into_iter()
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut posts_by_slug : HashMap<String, Arc<Post>> = HashMap::new();
|
||||||
|
let mut posts_by_tag : HashMap<String, Vec<Arc<Post>>> = 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_default();
|
||||||
|
posts.push(post.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Loaded {} posts", posts.len());
|
||||||
|
log::info!("Found {} tags", posts_by_tag.len());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
posts,
|
||||||
|
posts_by_slug,
|
||||||
|
posts_by_tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +1,43 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct PostMetadata {
|
pub struct PostMetadata {
|
||||||
|
pub slug: String,
|
||||||
pub image_path: String,
|
pub image_path: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub date: String,
|
pub date: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub project_link: String,
|
pub project_link: String,
|
||||||
pub draft: bool
|
pub draft: bool,
|
||||||
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub slug : String,
|
|
||||||
pub metadata: PostMetadata,
|
pub metadata: PostMetadata,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(feature = "ssr")] {
|
if #[cfg(feature = "ssr")] {
|
||||||
impl Post {
|
#[derive(Debug, thiserror::Error)]
|
||||||
fn from_content(content: String) -> Option<Self> {
|
pub enum PostDeserializationError {
|
||||||
|
#[error("Invalid front matter")]
|
||||||
|
InvalidFrontMatter,
|
||||||
|
#[error("Invalid markdown")]
|
||||||
|
InvalidMarkdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Post {
|
||||||
|
type Error = PostDeserializationError;
|
||||||
|
|
||||||
|
fn try_from(content: String) -> Result<Self, Self::Error> {
|
||||||
use gray_matter::{Matter, engine::YAML};
|
use gray_matter::{Matter, engine::YAML};
|
||||||
let matter = Matter::<YAML>::new();
|
let matter = Matter::<YAML>::new();
|
||||||
|
|
||||||
let post_data = matter
|
let post_data = matter
|
||||||
.parse_with_struct::<PostMetadata>(&content)?;
|
.parse_with_struct::<PostMetadata>(&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;
|
let content = post_data.content;
|
||||||
|
|
||||||
use pulldown_cmark::{Parser, Options, html};
|
use pulldown_cmark::{Parser, Options, html};
|
||||||
|
@ -36,49 +45,14 @@ cfg_if::cfg_if! {
|
||||||
let mut html_output = String::new();
|
let mut html_output = String::new();
|
||||||
html::push_html(&mut html_output, parser);
|
html::push_html(&mut html_output, parser);
|
||||||
|
|
||||||
Some(Self {
|
if html_output.is_empty() {
|
||||||
slug,
|
return Err(PostDeserializationError::InvalidMarkdown);
|
||||||
metadata,
|
|
||||||
content: html_output,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_path(path: &std::path::Path) -> Option<Self> {
|
|
||||||
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<Vec<Self>, String> {
|
|
||||||
use std::{path::Path, fs::read_dir};
|
|
||||||
|
|
||||||
let mut posts: Vec<Self> = Vec::new();
|
|
||||||
|
|
||||||
let folder_path = Path::new("markdowns").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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(posts)
|
Ok(Self {
|
||||||
|
metadata: post_data.data,
|
||||||
|
content: html_output,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,64 +9,113 @@ use crate::app::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_posts() -> Result<Vec<Post>, ServerFnError> {
|
pub async fn get_posts(
|
||||||
let posts = Post::get_all("posts")
|
tag: Option<String>
|
||||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
) -> Result<Vec<Post>, ServerFnError> {
|
||||||
|
leptos_actix::extract(
|
||||||
Ok(posts)
|
|data: actix_web::web::Data<crate::app::models::Data>| async move {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let default = vec![];
|
||||||
|
let posts = match tag {
|
||||||
|
Some(tag) => data.posts_by_tag.get(&tag).unwrap_or(&default),
|
||||||
|
None => &data.posts
|
||||||
|
};
|
||||||
|
posts.iter()
|
||||||
|
.map(|post| {
|
||||||
|
Post {
|
||||||
|
metadata: post.metadata.clone(),
|
||||||
|
content: post.content.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>, >()
|
||||||
|
},
|
||||||
|
).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_post(
|
pub async fn get_post(
|
||||||
slug: String
|
slug: String
|
||||||
) -> Result<Post, ServerFnError> {
|
) -> Result<Post, ServerFnError> {
|
||||||
let posts = Post::get_all("posts")
|
leptos_actix::extract(
|
||||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
|data: actix_web::web::Data<crate::app::models::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())))
|
||||||
|
}
|
||||||
|
|
||||||
let post = posts.into_iter().find(|post| post.slug == slug)
|
#[component]
|
||||||
.ok_or(ServerFnError::ServerError("Post not found".to_string()))?;
|
pub fn PostTags(
|
||||||
|
tags: Vec<String>
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="tags_list">
|
||||||
|
{
|
||||||
|
tags.into_iter().map(|tag| view! { <A class="tag" href=format!("/posts?tag={}", tag)>{tag}</A>}).collect_view()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(post)
|
#[component]
|
||||||
|
pub fn PostListCard(
|
||||||
|
post: Post
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<img src={post.metadata.image_path.clone()} alt=format!("Image {}", post.metadata.title)/>
|
||||||
|
|
||||||
|
{
|
||||||
|
if post.metadata.draft {
|
||||||
|
Some(view!{
|
||||||
|
<div class="warning">
|
||||||
|
<Icon icon=Icon::from(IoIcon::IoConstruct)/>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<PostTags tags=post.metadata.tags.clone()/>
|
||||||
|
<h2><A href=format!("/posts/{}", post.metadata.slug.clone())>{post.metadata.title.clone()}</A></h2>
|
||||||
|
<p>{post.metadata.description.clone()}</p>
|
||||||
|
<span>{post.metadata.date.clone()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostList() -> impl IntoView {
|
pub fn PostList() -> impl IntoView {
|
||||||
let posts = create_resource(|| (), |_| get_posts());
|
let query = use_query_map();
|
||||||
|
let tag = move || query.with(|query| query.get("tag").cloned());
|
||||||
|
let posts = create_resource(move || tag(), move |_| get_posts(tag()));
|
||||||
|
|
||||||
let posts_view = move || {
|
let posts_view = move || {
|
||||||
posts.and_then(|posts| {
|
posts.and_then(|posts| {
|
||||||
posts.iter()
|
posts.iter()
|
||||||
.map(|post| view! {
|
.map(|post| view! { <PostListCard post=post.clone() /> })
|
||||||
<a href=format!("posts/{}", post.slug.clone())>
|
|
||||||
<img src={post.metadata.image_path.clone()} alt=format!("Image {}", post.metadata.title)/>
|
|
||||||
|
|
||||||
{
|
|
||||||
if post.metadata.draft {
|
|
||||||
Some(view!{
|
|
||||||
<div class="warning">
|
|
||||||
<Icon icon=Icon::from(IoIcon::IoConstruct)/>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>{post.metadata.title.clone()}</h2>
|
|
||||||
<p>{post.metadata.description.clone()}</p>
|
|
||||||
<span>{post.metadata.date.clone()}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
})
|
|
||||||
.collect_view()
|
.collect_view()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let title = move || match tag() {
|
||||||
|
Some(tag) => view! { <Title href="/posts".to_string() title=format!("Posts for {}", tag)/> },
|
||||||
|
None => view! { <Title href="/".to_string() title="Posts".to_string()/> }
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense fallback=move || view! { <Loading title="Chargement des posts...".to_string() /> }>
|
<Suspense fallback=move || view! { <Loading title="Chargement des posts...".to_string() /> }>
|
||||||
<main class="posts">
|
<main class="posts">
|
||||||
<Title href="/".to_string() title="Posts".to_string()/>
|
{ title }
|
||||||
|
|
||||||
<div class="posts__cards">{posts_view}</div>
|
<div class="posts__cards">{posts_view}</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -86,6 +135,7 @@ pub fn PostElement() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<>
|
<>
|
||||||
<Title href="/posts".to_string() title=post.metadata.title.clone()/>
|
<Title href="/posts".to_string() title=post.metadata.title.clone()/>
|
||||||
|
<PostTags tags=post.metadata.tags.clone()/>
|
||||||
{
|
{
|
||||||
if post.metadata.draft {
|
if post.metadata.draft {
|
||||||
Some(view!{
|
Some(view!{
|
||||||
|
|
33
src/app/utils/data_src.rs
Normal file
33
src/app/utils/data_src.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#[allow(dead_code)] // Use in main.rs
|
||||||
|
pub fn get_all<T>(folder: &str) -> anyhow::Result<Vec<T>>
|
||||||
|
where
|
||||||
|
T: TryFrom<String>,
|
||||||
|
{
|
||||||
|
use std::{path::Path, fs::read_dir};
|
||||||
|
|
||||||
|
let mut datas: Vec<T> = 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<T, String> = 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) => log::warn!("{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(datas)
|
||||||
|
}
|
1
src/app/utils/mod.rs
Normal file
1
src/app/utils/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub(crate) mod data_src;
|
|
@ -12,10 +12,10 @@ cfg_if! {
|
||||||
_ = console_log::init_with_level(log::Level::Debug);
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
logging::log!("hydrate mode - hydrating");
|
log::info!("hydrate mode - hydrating");
|
||||||
logging::log!("Portfolio version : {}", env!("CARGO_PKG_VERSION"));
|
log::info!("Portfolio version : {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
leptos::mount_to_body(|| {
|
mount_to_body(|| {
|
||||||
view! { <App/> }
|
view! { <App/> }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,8 @@ cfg_if! {
|
||||||
_ = console_log::init_with_level(log::Level::Debug);
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
logging::log!("csr mode - mounting to body");
|
log::info!("csr mode - mounting to body");
|
||||||
|
log::info!("Portfolio version : {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
mount_to_body(|| {
|
mount_to_body(|| {
|
||||||
view! { <App /> }
|
view! { <App /> }
|
||||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -8,9 +8,26 @@ cfg_if! {
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use crate::app::*;
|
use crate::app::*;
|
||||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
|
|
||||||
|
#[get("/style.css")]
|
||||||
|
async fn css() -> impl Responder {
|
||||||
|
actix_files::NamedFile::open_async("./style/output.css").await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// Configure logger
|
||||||
|
SimpleLogger::new()
|
||||||
|
.with_level(log::LevelFilter::Info)
|
||||||
|
.env()
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Load required data
|
||||||
|
let data = web::Data::<models::Data>::new(models::Data::new().unwrap());
|
||||||
|
|
||||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||||
let conf = get_configuration(None).await.unwrap();
|
let conf = get_configuration(None).await.unwrap();
|
||||||
|
|
||||||
|
@ -24,8 +41,10 @@ cfg_if! {
|
||||||
let site_root = &leptos_options.site_root;
|
let site_root = &leptos_options.site_root;
|
||||||
let routes = &routes;
|
let routes = &routes;
|
||||||
App::new()
|
App::new()
|
||||||
|
.app_data(web::Data::clone(&data)) // Must panic if data can't be loaded
|
||||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> })
|
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> })
|
||||||
|
.service(css)
|
||||||
.service(Files::new("/", site_root))
|
.service(Files::new("/", site_root))
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
})
|
})
|
||||||
|
|
0
style/main.scss
Normal file
0
style/main.scss
Normal file
|
@ -42,13 +42,21 @@
|
||||||
& ol li {
|
& ol li {
|
||||||
@apply list-decimal list-inside;
|
@apply list-decimal list-inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& blockquote {
|
||||||
|
@apply border-l-4 border-primary/50 dark:border-dark_primary/50 pl-3 my-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .tags_list {
|
||||||
|
@apply flex flex-row flex-wrap gap-2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts {
|
.posts {
|
||||||
& > .posts__cards {
|
& > .posts__cards {
|
||||||
@apply grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 m-5;
|
@apply grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 m-5;
|
||||||
|
|
||||||
& > a {
|
& > div {
|
||||||
@apply rounded-xl overflow-hidden relative;
|
@apply rounded-xl overflow-hidden relative;
|
||||||
@apply bg-primary/10 dark:bg-dark_primary/10;
|
@apply bg-primary/10 dark:bg-dark_primary/10;
|
||||||
@apply shadow-md shadow-primary/5 dark:shadow-dark_primary/5;
|
@apply shadow-md shadow-primary/5 dark:shadow-dark_primary/5;
|
||||||
|
|
Loading…
Add table
Reference in a new issue