use std::borrow::Borrow; use std::collections::HashSet; use std::hash::Hash; use dioxus::prelude::*; use tavern::{Adventurer, Legend, Lore, Tale}; use crate::page::Page; use crate::server::*; const AUTHOR_CSS: Asset = asset!("/assets/css/blog_author.css"); const LIST_CSS: Asset = asset!("/assets/css/blog_list.css"); const NAV_CSS: Asset = asset!("/assets/css/blog_nav.css"); const POST_CSS: Asset = asset!("/assets/css/blog_post.css"); const TAG_CSS: Asset = asset!("/assets/css/blog_tag.css"); /// Trait to toggle the presence of a value (like a tag) in a set. pub trait Togglable { type Item: ?Sized; fn toggle(&mut self, item: &Self::Item); fn is_toggled(&self, item: &Self::Item) -> bool; } impl Togglable for HashSet { type Item = str; fn toggle(&mut self, item: &str) { if self.contains(item) { self.remove(item); } else { self.insert(item.to_owned()); } } fn is_toggled(&self, item: &str) -> bool { self.contains(item) } } fn convert_tag(text: S) -> (String, String) where S: AsRef { if let Some((first, second)) = text.as_ref().split_once('-') { (first.to_owned(), second.to_owned()) } else { (text.as_ref().to_owned(), "none".to_owned()) } } fn strip_tag(text: &str) -> &str { if let Some((first, _)) = text.split_once('-') { first } else { text } } #[component] pub fn TagItem(tag: String, style: String) -> Element { rsx! { li { class: "tag_item", Link { class: "{style}", to: Page::Blog { tag: tag.clone() }, "{tag}" } // a { class: "{style}", "{tag}" } } } } #[component] pub fn TagList(children: Element) -> Element { rsx! { document::Link { rel: "stylesheet", href: TAG_CSS } h5 { class: "blog_tag_style", ul { class: "tag_list", {children} } } } } #[component] pub fn BlogAuthor() -> Element { rsx! { document::Link { rel: "stylesheet", href: AUTHOR_CSS } section { class: "blog_author_style", } } } #[component] pub fn BlogItem(title: String, slug: String, author: String, summary: String, tags: Vec) -> Element { let tags: Vec<(String, String)> = tags.iter().map(|t| convert_tag(t)).collect(); rsx! { li { class: "blog_item", Link { to: Page::Post { slug: slug }, h1 { "{title}" } } TagList { for (tag, style) in tags { TagItem { tag: tag.clone(), style: style.clone() } } } h4 { "Author: {author}" } p { "{summary}" } } } } #[component] pub fn BlogList(tags: Signal>, children: Element) -> Element { let list = use_server_future(move || { let categories = tags().iter().cloned().collect(); async move { get_blog_list(categories).await } })?; // The tale depends on the post future resolving. let mut summaries: Signal>> = use_signal(|| None); use_context_provider(|| summaries); use_effect(move || { if let Some(Ok(data)) = &*list.read() { summaries.set(Some(data.clone())); } }); rsx! { document::Link { rel: "stylesheet", href: LIST_CSS } section { class: "blog_list_style", ul { class: "blog_list", match &*list.read() { Some(Ok(lores)) => { rsx! { for lore in lores { BlogItem { title: lore.title.clone(), slug: lore.slug.clone(), author: lore.author.clone(), summary: lore.summary.clone(), tags: lore.tags.clone() } } } } Some(Err(e)) => { rsx! { p { "Unable to show post header." } p { "{e}" } } } None => { rsx! { p { "Loading..." } } } } {children} } } } } #[component] pub fn TagSelector(toggled_tags: Signal>) -> Element { let mut categories = use_signal(|| Vec::::new()); let tags = use_server_future(move || async move { get_tags().await })?; use_effect(move || { if let Some(Ok(ref tags_vec)) = *tags.read() { categories.set(tags_vec.clone()); } }); rsx! { document::Link { rel: "stylesheet", href: NAV_CSS } section { class: "blog_nav_style", div { class: "tag_style", h2 { "Categories" } ul { class: "tag_list", if categories.read().is_empty() { p { "Loading..." } } else { for tag in categories.read().iter().cloned() { li { class: "tag_item", span { class: if toggled_tags.read().is_toggled(&tag) { "tag selected" } else { "tag" }, onclick: move |_| { toggled_tags.write().toggle(&tag); }, "{strip_tag(&tag)}" } } } } } } } } } #[component] pub fn PostHeaderAuthor(adventurer: Signal>) -> Element { rsx! { match adventurer() { Some(author) => { rsx! { h4 { "Author: ", a { href: "{author.legend.profile}", "{author.name} @{author.handle}" } } } } None => { rsx! { h4 { "Author: Unknown" } } } } } } #[component] pub fn PostHeader(tale: Signal>, adventurer: Signal>) -> Element { let converted_tags = use_memo(move || { tale().as_ref() .map(|t| { t.lore .tags .iter() .map(|tag| convert_tag(tag)) .collect::>() }) .unwrap_or_default() }); rsx! { match tale() { Some(tale) => { rsx! { h1 { {tale.lore.title} } TagList { for (tag, style) in converted_tags().iter() { TagItem { tag: tag.clone(), style: style.clone() } } } } } None => { rsx! { p { "Loading post header..." } } } } match adventurer() { Some(_) => { rsx! { PostHeaderAuthor { adventurer: adventurer } } } None => { rsx! { p { "Loading author..." } } } } } } #[component] pub fn BlogPost(slug: Signal, children: Element) -> Element { // The tale depends on the post future resolving. let mut tale: Signal> = use_signal(|| None); let mut adventurer = use_signal(|| None); // Run a once off server fetch of the unchanging blog data. let post = use_server_future(move || { // Make this reactive so that as the page changes it should rerun this. let url_slug = slug(); async move { get_blog_post(url_slug).await } })?; let author = use_server_future(move || { let handle = match tale() { Some(data) => data.lore.author, None => "unknown_author_handle".to_string() }; async move { get_author(handle).await } })?; use_effect(move || { if let Some(Ok(data)) = &*post.read() { tale.set(Some(data.clone())); } }); use_effect(move || { if let Some(Ok(data)) = &*author.read() { adventurer.set(Some(data.clone())); } }); rsx! { document::Link { rel: "stylesheet", href: POST_CSS } article { class: "blog_post_style", match &*post.read() { Some(Ok(post)) => { rsx! { PostHeader { tale: tale, adventurer: adventurer } //Story {} div { dangerous_inner_html: "{post.story}" } //Author {} } } Some(Err(e)) => { rsx! { p { "Unable to load desired post: {slug.read()}" } p { "{e}" } } } None => { rsx! { p { "Loading..." } } } } {children} } } } #[component] pub fn TagNav() -> Element { let tags = use_server_future(move || async move { get_tags().await })?; rsx! { document::Link { rel: "stylesheet", href: NAV_CSS } section { class: "blog_nav_style", div { class: "tag_style", h2 { "Categories" } ul { class: "tag_list", match &*tags.read() { Some(Ok(categories)) => { rsx! { for tag in categories { li { class: "tag_item", a { href: "/blog/{strip_tag(tag)}", "{strip_tag(tag)}" } } } } } Some(Err(e)) => { rsx! { p { "Unable to show desired post." } p { "{e}" } } } None => { rsx! { p { "Loading..." } } } } } } } } }