From 967957897762bc00f78601f94b9db0cff266aa64 Mon Sep 17 00:00:00 2001 From: Myrddin Dundragon Date: Sat, 6 Sep 2025 15:05:58 -0400 Subject: [PATCH] Adding components and assets. Commiting to try the library with the target dioxus project. --- bard/Cargo.toml | 8 +- bard/assets/css/blog.css | 19 +++ bard/assets/css/blog_author.css | 230 ++++++++++++++++++++++++++++++++ bard/assets/css/blog_list.css | 27 ++++ bard/assets/css/blog_nav.css | 116 ++++++++++++++++ bard/assets/css/blog_post.css | 117 ++++++++++++++++ bard/assets/css/blog_tag.css | 54 ++++++++ bard/src/components.rs | 122 +++++++++++++++-- bard/src/lib.rs | 17 +-- bard/src/server.rs | 71 ++++++++-- 10 files changed, 743 insertions(+), 38 deletions(-) create mode 100644 bard/assets/css/blog.css create mode 100644 bard/assets/css/blog_author.css create mode 100644 bard/assets/css/blog_list.css create mode 100644 bard/assets/css/blog_nav.css create mode 100644 bard/assets/css/blog_post.css create mode 100644 bard/assets/css/blog_tag.css diff --git a/bard/Cargo.toml b/bard/Cargo.toml index 4b5b885..315c48b 100644 --- a/bard/Cargo.toml +++ b/bard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bard" -version = "0.0.2" +version = "0.0.3" edition = "2024" description = "Dioxus components that will display a Tavern blogging system Blog." repository = "/CyberMages/tavern" @@ -10,8 +10,10 @@ readme = "README.md" license = "Apache-2.0" [dependencies] +dioxus = { version = "*", features = ["router", "fullstack"], optional = true } tavern = { version = "0.2.4", path = "../tavern", registry = "cybermages", optional = true} +tokio = { version = "1.0", features = ["full"], optional = true } [features] -default = ["tavern"] -server = ["tavern/database"] +default = ["tavern", "dioxus/web"] +server = ["tavern/database", "dioxus/server", "tokio"] diff --git a/bard/assets/css/blog.css b/bard/assets/css/blog.css new file mode 100644 index 0000000..b4b9bde --- /dev/null +++ b/bard/assets/css/blog.css @@ -0,0 +1,19 @@ +.blog_style +{ + display: flex; + width: 100%; + height: 100%; + background: var(--bg-color); + padding: 50px 0px; + justify-content: center; + container-name: site; + container-type: inline-size; + + .page_content + { + display: flex; + width: 67%; + container-name: page; + container-type: inline-size; + } +} diff --git a/bard/assets/css/blog_author.css b/bard/assets/css/blog_author.css new file mode 100644 index 0000000..573a8a5 --- /dev/null +++ b/bard/assets/css/blog_author.css @@ -0,0 +1,230 @@ +.blog_style +{ + display: flex; + width: 100%; + height: 100%; + background: var(--bg-color); + padding: 50px 0px; + justify-content: center; + container-name: site; + container-type: inline-size; + + .page_content + { + display: flex; + width: 67%; + container-name: page; + container-type: inline-size; + } +} + + + +.blog_nav +{ + width: 15%; + padding: 0px 20px; + + h3 + { + margin-bottom: 10px; + } + + ul + { + list-style: none; + + li + { + } + } + + @container site (max-width: 1230px) + { + display: none; + } + + .social + { + /* Hide .social until the font is loaded */ + display: flex; + align-items: center; + gap: 15px; + margin: 25px 0; + font-family: cm_social; + + a + { + color: var(--text-color); + font-size: 46px; + text-decoration: none; + background: transparent; + width: 65px; + height: 65px; + border-radius: 50%; + display: flex; + align-items: flex-end; + justify-content: center; + transition: 0.4s ease; + border: 4px solid var(--text-color); + + &:hover + { + color: var(--accent-color); + border: 4px solid var(--accent-color); + transform: translateY(-7px); + } + } + } +} +} + + + +.blog_item_area +{ + .blog_list + { + list-style: none; + + .blog_item + { + margin-bottom: 50px; + + h1 + { + color: var(--accent-color); + } + + h4 + { + margin: 10px 0px; + } + + p + { + display: block; + } + } + } +} + + + +.blog_article +{ + h1 + { + color: var(--accent-color); + } + + h4 + { + margin-top: 10px; + margin-bottom: 30px; + } + + p + { + margin-bottom: 20px; + } + + .embeded_video + { + margin-top: 50px; + display: flex; + align-content: center; + justify-content: center; + + iframe + { + box-shadow: 0 0 80px var(--accent-color); + } + } +} + + + +.tag_style +{ + display: block; + + .tag_list + { + list-style: none; + + .tag_item + { + display: inline-block; + margin: 5px 5px 0px 0px; + + a + { + display: block; + padding: 0px 4px; + text-decoration: none; + border: 2px solid var(--text-color); + border-radius: 5px; + + color: var(--text-color); + transition: 0.1s ease-in; + + &:hover + { + color: var(--accent-color); + border: 2px solid var(--accent-color); + transform: translateY(-2px); + } + } + } + } +} + + + +.easy +{ + background-color: #248721; +} + +.medium +{ + background-color: #d6a318; +} + +.hard +{ + background-color: #d92121; +} + + + +.social +{ + background: transparent; + font-family: cm_social; + margin: 15px 0px; + + a + { + color: var(--text-color); + font-size: 30px; + text-decoration: none; + background: transparent; + width: 45px; + height: 45px; + border-radius: 50%; + display: flex; + align-items: flex-end; + justify-content: center; + transition: 0.4s ease; + border: 4px solid var(--text-color); + + &:hover + { + color: var(--accent-color); + border: 4px solid var(--accent-color); + transform: translateY(-7px); + } + } +} diff --git a/bard/assets/css/blog_list.css b/bard/assets/css/blog_list.css new file mode 100644 index 0000000..c7a3af5 --- /dev/null +++ b/bard/assets/css/blog_list.css @@ -0,0 +1,27 @@ +.blog_list_style +{ + .blog_list + { + list-style: none; + + .blog_item + { + margin-bottom: 50px; + + h1 + { + color: var(--accent-color); + } + + h4 + { + margin: 10px 0px; + } + + p + { + display: block; + } + } + } +} diff --git a/bard/assets/css/blog_nav.css b/bard/assets/css/blog_nav.css new file mode 100644 index 0000000..c22149a --- /dev/null +++ b/bard/assets/css/blog_nav.css @@ -0,0 +1,116 @@ +.blog_style +{ + display: flex; + width: 100%; + height: 100%; + background: var(--bg-color); + padding: 50px 0px; + justify-content: center; + container-name: site; + container-type: inline-size; + + .page_content + { + display: flex; + width: 67%; + container-name: page; + container-type: inline-size; + } +} + + + +.blog_nav_style +{ + width: 15%; + padding: 0px 20px; + + h3 + { + margin-bottom: 10px; + } + + ul + { + list-style: none; + + li + { + } + } + + @container site (max-width: 1230px) + { + display: none; + } + + .social + { + /* Hide .social until the font is loaded */ + display: flex; + align-items: center; + gap: 15px; + margin: 25px 0; + font-family: cm_social; + + a + { + color: var(--text-color); + font-size: 22px; + text-decoration: none; + background: transparent; + width: 35px; + height: 35px; + border-radius: 50%; + display: flex; + align-items: flex-end; + justify-content: center; + transition: 0.4s ease; + border: 4px solid var(--text-color); + + &:hover + { + color: var(--accent-color); + border: 4px solid var(--accent-color); + transform: translateY(-7px); + } + } + } + +.tag_style +{ + display: block; + + .tag_list + { + display: inline-flex; + flex-direction: column; + list-style: none; + + .tag_item + { + display: inline-block; + margin: 5px 5px 0px 0px; + + a + { + display: inline-flex; + padding: 0px 4px; + text-decoration: none; + border: 2px solid var(--text-color); + border-radius: 5px; + + color: var(--text-color); + transition: 0.1s ease-in; + + &:hover + { + color: var(--accent-color); + border: 2px solid var(--accent-color); + transform: translateY(-2px); + } + } + } + } +} +} diff --git a/bard/assets/css/blog_post.css b/bard/assets/css/blog_post.css new file mode 100644 index 0000000..6df9db2 --- /dev/null +++ b/bard/assets/css/blog_post.css @@ -0,0 +1,117 @@ +.blog_post_style +{ + h1 + { + color: var(--accent-color); + } + + h4 + { + margin-top: 10px; + margin-bottom: 30px; + } + + p + { + margin-bottom: 20px; + } + + .embeded_video + { + margin-top: 50px; + display: flex; + align-content: center; + justify-content: center; + + iframe + { + box-shadow: 0 0 80px var(--accent-color); + } + } + + + + +.tag_style +{ + display: block; + + .tag_list + { + list-style: none; + + .tag_item + { + display: inline-block; + margin: 5px 5px 0px 0px; + + a + { + display: block; + padding: 0px 4px; + text-decoration: none; + border: 2px solid var(--text-color); + border-radius: 5px; + + color: var(--text-color); + transition: 0.1s ease-in; + + &:hover + { + color: var(--accent-color); + border: 2px solid var(--accent-color); + transform: translateY(-2px); + } + } + } + } +} + + + +.easy +{ + background-color: #248721; +} + +.medium +{ + background-color: #d6a318; +} + +.hard +{ + background-color: #d92121; +} + + + +.social +{ + background: transparent; + font-family: cm_social; + margin: 15px 0px; + + a + { + color: var(--text-color); + font-size: 30px; + text-decoration: none; + background: transparent; + width: 45px; + height: 45px; + border-radius: 50%; + display: flex; + align-items: flex-end; + justify-content: center; + transition: 0.4s ease; + border: 4px solid var(--text-color); + + &:hover + { + color: var(--accent-color); + border: 4px solid var(--accent-color); + transform: translateY(-7px); + } + } +} diff --git a/bard/assets/css/blog_tag.css b/bard/assets/css/blog_tag.css new file mode 100644 index 0000000..1f12a6d --- /dev/null +++ b/bard/assets/css/blog_tag.css @@ -0,0 +1,54 @@ +.blog_tag_style +{ + display: block; + + .tag_list + { + list-style: none; + + .tag_item + { + display: inline-block; + margin: 5px 5px 0px 0px; + + a + { + display: block; + padding: 0px 4px; + text-decoration: none; + border: 2px solid var(--text-color); + border-radius: 5px; + + color: var(--text-color); + transition: 0.1s ease-in; + + &:hover + { + color: var(--accent-color); + border: 2px solid var(--accent-color); + transform: translateY(-2px); + } + } + } + } + + .none + { + background-color: var(--bg-color); + } + + .easy + { + background-color: #248721; + } + + .medium + { + background-color: #d6a318; + } + + .hard + { + background-color: #d92121; + } +} diff --git a/bard/src/components.rs b/bard/src/components.rs index 2c1895b..267e7b2 100644 --- a/bard/src/components.rs +++ b/bard/src/components.rs @@ -1,10 +1,9 @@ use dioxus::prelude::*; -use tavern::Lore; +use tavern::{Adventurer, Legend, Lore, Tale}; -use crate::components::social::*; -use crate::page::Page; +use crate::server::*; @@ -15,7 +14,6 @@ const POST_CSS: Asset = asset!("/assets/css/blog_post.css"); const TAG_CSS: Asset = asset!("/assets/css/blog_tag.css"); - #[derive(Clone, Copy, PartialEq)] pub enum TagStyle { @@ -85,6 +83,7 @@ pub fn TagList(children: Element) -> Element } +/* #[component] pub fn BlogAuthor() -> Element { @@ -210,20 +209,32 @@ pub fn BlogNav() -> Element } } } +*/ #[component] -pub fn BlogPost(children: Element) -> Element +pub fn PostHeaderAuthor(name: String, handle: String, profile_link: String) -> Element { rsx! { - document::Link { rel: "stylesheet", href: POST_CSS } + h4 { "Author: ", a { href: "{profile_link}", "{name} @{handle}" } } + } +} - article +#[component] +pub fn PostHeader(title: String, author: String, tags: Vec) -> Element +{ + let Ok(author) = use_server_future(move || get_author(author.clone())) else + { + return rsx! { p { "Failed to load author." } } + }; + + rsx! + { + h1 { "{title}" } + TagList { - class: "blog_post_style", - h1 { "Leet Code: Let's Get Started" } - TagList + for category in tags { TagItem { @@ -231,7 +242,96 @@ pub fn BlogPost(children: Element) -> Element style: TagStyle::Regular } } - h4 { "Author: ", a { href: "https://cybermages.tech", "Jason Smith" } } + } + + match &*author.read() + { + Some(Ok(author)) => + { + rsx! + { + PostHeaderAuthor + { + name: author.name.clone(), + handle: author.handle.clone(), + profile_link: author.legend.profile.clone() + } + } + } + + Some(Err(e)) => + { + rsx! + { + p { "Unable to show post header." } + p { "{e}" } + } + } + + None => + { + rsx! + { + p { "Loading..." } + } + } + } + } +} + +#[component] +pub fn BlogPost(slug: String, children: Element) -> Element +{ + // 1. Fetch the blog post using the slug. + let Ok(post) = use_server_future(move || get_blog_post(slug.clone())) else + { + return rsx! { p { "Failed to load post." } } + }; + + // Then build the component. + rsx! + { + document::Link { rel: "stylesheet", href: POST_CSS } + + article + { + class: "blog_post_style", + + match &*post.read() + { + Some(Ok(post)) => + { + rsx! + { + PostHeader + { + title: post.lore.title.clone(), + author: post.lore.author.clone(), + tags: post.lore.tags.clone() + } + + div { dangerous_inner_html: "{post.story}" } + } + } + + Some(Err(e)) => + { + rsx! + { + p { "Unable to show desired post." } + p { "{e}" } + } + } + + None => + { + rsx! + { + p { "Loading..." } + } + } + } + {children} } } diff --git a/bard/src/lib.rs b/bard/src/lib.rs index 96a050a..0248f84 100644 --- a/bard/src/lib.rs +++ b/bard/src/lib.rs @@ -2,20 +2,17 @@ mod info; +#[cfg(not(feature = "server"))] +mod components; + +mod server; -#[cfg(feature = "server")] -use tavern::Database; pub use crate::info::{get_name, get_version}; - +#[cfg(not(feature = "server"))] +pub use crate::components::*; #[cfg(feature = "server")] -pub async fn init_database

(path: P) - -> Result, Box> - where P: AsRef -{ - let db = Database::open(path).await?; - Ok(std::sync::Arc::new(db)) -} +pub use crate::server::*; diff --git a/bard/src/server.rs b/bard/src/server.rs index 81e5bfa..ded62e2 100644 --- a/bard/src/server.rs +++ b/bard/src/server.rs @@ -1,27 +1,70 @@ -use dioxus::prelude::*; +// Remember that Server functions parameters must not be references. +// They are coming from potentially other computers. + +#[cfg(feature = "server")] use std::sync::Arc; -use crate::{Database, Lore, Tale}; + +use dioxus::prelude::*; + +#[cfg(feature = "server")] +use tokio::sync::OnceCell; + +use tavern::{Adventurer, Lore, Tale}; + +#[cfg(feature = "server")] +use tavern::Database; + + + +#[cfg(feature = "server")] +static BLOG_DATABASE: OnceCell> = OnceCell::const_new(); + + + +#[cfg(feature = "server")] +async fn get_database_instance() -> &'static Arc +{ + BLOG_DATABASE.get_or_init(|| async { + let db = Database::open("tavern.db") + .await + .expect("Failed to open database"); + Arc::new(db) + }).await +} + + #[server] -pub async fn get_blog_list() -> Result, ServerFnError> { - let db = server_context() - .get::>() - .ok_or_else(|| ServerFnError::ServerError("Database context not available".to_string()))?; +pub async fn get_blog_list(categories: Vec) -> Result, ServerFnError> +{ + let db = get_database_instance().await; - let summaries = db.get_tales_summary(&[]).await - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + let summaries = db.get_tales_summary(&categories).await + .map_err(|e| ServerFnError::new(e))?; Ok(summaries) } + #[server] -pub async fn get_blog_post(slug: String) -> Result { - let db = server_context() - .get::>() - .ok_or_else(|| ServerFnError::ServerError("Database context not available".to_string()))?; +pub async fn get_blog_post(slug: String) -> Result +{ + let db = get_database_instance().await; let tale = db.get_tale_by_slug(&slug).await - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + .map_err(|e| ServerFnError::new(e))?; - tale.ok_or(ServerFnError::ServerError("Post not found".into())) + tale.ok_or(ServerFnError::new(format!("Post {} not found", slug))) +} + + +#[server] +pub async fn get_author(handle: String) -> Result +{ + let db = get_database_instance().await; + + let author = db.get_adventurer(&handle).await + .map_err(|e| ServerFnError::new(e))?; + + author.ok_or(ServerFnError::new(format!("Author {} not found.", handle))) }