Adding more reactive pieces to our components.
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bard"
|
||||
version = "0.0.18"
|
||||
version = "0.0.19"
|
||||
edition = "2024"
|
||||
description = "Dioxus components that will display a Tavern blogging system Blog."
|
||||
repository = "/CyberMages/tavern"
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
{
|
||||
width: 15%;
|
||||
padding: 0px 20px;
|
||||
order: 5;
|
||||
|
||||
h3
|
||||
{
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
.blog_post_style
|
||||
{
|
||||
order: 1;
|
||||
|
||||
h1
|
||||
{
|
||||
color: var(--accent-color);
|
||||
@ -28,7 +30,7 @@
|
||||
box-shadow: 0 0 80px var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||
|
||||
@ -14,6 +18,40 @@ 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<String>
|
||||
{
|
||||
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<S>(text: S) -> (String, String)
|
||||
where S: AsRef<str>
|
||||
{
|
||||
@ -45,18 +83,18 @@ fn strip_tag(text: &str) -> &str
|
||||
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}" }
|
||||
li
|
||||
{
|
||||
class: "tag_item",
|
||||
Link
|
||||
{
|
||||
class: "{style}",
|
||||
to: Page::Blog { tag: tag.clone() },
|
||||
"{tag}"
|
||||
}
|
||||
// a { class: "{style}", "{tag}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@ -129,13 +167,25 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn BlogList(tags: Vec<String>, children: Element) -> Element
|
||||
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
||||
{
|
||||
let summaries = use_server_future(move || {
|
||||
let categories = tags.clone();
|
||||
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<Option<Vec<Lore>>> = 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 }
|
||||
|
||||
@ -146,7 +196,7 @@ pub fn BlogList(tags: Vec<String>, children: Element) -> Element
|
||||
{
|
||||
class: "blog_list",
|
||||
|
||||
match &*summaries.read()
|
||||
match &*list.read()
|
||||
{
|
||||
Some(Ok(lores)) =>
|
||||
{
|
||||
@ -191,11 +241,22 @@ pub fn BlogList(tags: Vec<String>, children: Element) -> Element
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlogNav() -> Element
|
||||
pub fn TagSelector(toggled_tags: Signal<HashSet<String>>) -> Element
|
||||
{
|
||||
let mut categories = use_signal(Vec::<String>::new);
|
||||
|
||||
let tags = use_server_future(move || async move { get_tags().await })?;
|
||||
|
||||
rsx! {
|
||||
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
|
||||
@ -204,129 +265,178 @@ pub fn BlogNav() -> Element
|
||||
div
|
||||
{
|
||||
class: "tag_style",
|
||||
h3 { "Categories" }
|
||||
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!
|
||||
if categories.read().is_empty()
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn PostHeaderAuthor(name: String, handle: String, profile_link: String)
|
||||
-> Element
|
||||
{
|
||||
rsx! {
|
||||
h4 { "Author: ", a { href: "{profile_link}", "{name} @{handle}" } }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PostHeader(title: String, author: String, tags: Vec<String>) -> Element
|
||||
{
|
||||
let author = use_server_future(move || {
|
||||
let target_author = author.clone();
|
||||
async move { get_author(target_author).await }
|
||||
})?;
|
||||
|
||||
|
||||
let converted_tags: Vec<(String, String)> =
|
||||
tags.iter().map(|t| convert_tag(t)).collect();
|
||||
|
||||
rsx! {
|
||||
h1 { "{title}" }
|
||||
TagList
|
||||
{
|
||||
for (tag, style) in converted_tags
|
||||
{
|
||||
TagItem
|
||||
{
|
||||
tag: tag.clone(),
|
||||
style: style.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &*author.read()
|
||||
{
|
||||
Some(Ok(author)) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
PostHeaderAuthor
|
||||
else
|
||||
{
|
||||
name: author.name.clone(),
|
||||
handle: author.handle.clone(),
|
||||
profile_link: author.legend.profile.clone()
|
||||
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)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Err(e)) =>
|
||||
#[component]
|
||||
pub fn PostHeaderAuthor() -> Element
|
||||
{
|
||||
let author: Signal<Option<Adventurer>> =
|
||||
use_context::<Signal<Option<Adventurer>>>();
|
||||
|
||||
rsx! {
|
||||
match author()
|
||||
{
|
||||
Some(author) =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
p { "Unable to show post header." }
|
||||
p { "{e}" }
|
||||
h4
|
||||
{
|
||||
"Author: ",
|
||||
a { href: "{author.legend.profile}", "{author.name} @{author.handle}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
p { "Loading..." }
|
||||
}
|
||||
rsx! { h4 { "Author: Unknown" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlogPost(slug: String, children: Element) -> Element
|
||||
pub fn PostHeader() -> Element
|
||||
{
|
||||
let tale: Signal<Option<Tale>> = use_context::<Signal<Option<Tale>>>();
|
||||
let adventurer = use_context::<Signal<Option<Adventurer>>>();
|
||||
|
||||
let converted_tags = use_memo(move || {
|
||||
tale().as_ref()
|
||||
.map(|t| {
|
||||
t.lore
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| convert_tag(tag))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.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(author) =>
|
||||
{
|
||||
rsx! { PostHeaderAuthor {} }
|
||||
}
|
||||
|
||||
None =>
|
||||
{
|
||||
rsx! { p { "Loading author..." } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||
{
|
||||
// Run a once off server fetch of the unchanging blog data.
|
||||
let post = use_server_future(move || {
|
||||
let url_slug = slug.clone();
|
||||
// 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 }
|
||||
})?;
|
||||
|
||||
// The tale depends on the post future resolving.
|
||||
let mut tale: Signal<Option<Tale>> = use_signal(|| None);
|
||||
use_context_provider(|| tale);
|
||||
|
||||
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 }
|
||||
})?;
|
||||
|
||||
let mut adventurer = use_signal(|| None);
|
||||
use_context_provider(|| adventurer);
|
||||
|
||||
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 }
|
||||
|
||||
@ -340,14 +450,13 @@ pub fn BlogPost(slug: String, children: Element) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
PostHeader
|
||||
{
|
||||
title: post.lore.title.clone(),
|
||||
author: post.lore.author.clone(),
|
||||
tags: post.lore.tags.clone()
|
||||
}
|
||||
//PostHeader {}
|
||||
|
||||
//Story {}
|
||||
|
||||
div { dangerous_inner_html: "{post.story}" }
|
||||
|
||||
//Author {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,7 +464,7 @@ pub fn BlogPost(slug: String, children: Element) -> Element
|
||||
{
|
||||
rsx!
|
||||
{
|
||||
p { "Unable to show desired post." }
|
||||
p { "Unable to load desired post: {slug.read()}" }
|
||||
p { "{e}" }
|
||||
}
|
||||
}
|
||||
@ -373,3 +482,62 @@ pub fn BlogPost(slug: String, children: Element) -> Element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::BlogList;
|
||||
@ -9,15 +11,30 @@ const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
|
||||
|
||||
|
||||
fn convert_categories(categories: &str) -> HashSet<String>
|
||||
{
|
||||
if categories.is_empty() || categories == "all"
|
||||
{
|
||||
HashSet::new()
|
||||
}
|
||||
else
|
||||
{
|
||||
categories.split('+').map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Blog page
|
||||
#[component]
|
||||
pub fn Blog(tag: String) -> Element
|
||||
{
|
||||
let mut categories: Vec<String> = vec![];
|
||||
let mut categories: Signal<HashSet<String>> =
|
||||
use_signal(|| convert_categories(&tag));
|
||||
|
||||
if !tag.is_empty() && tag != "all"
|
||||
if *categories.read() != convert_categories(&tag)
|
||||
{
|
||||
categories.push(tag);
|
||||
categories.set(convert_categories(&tag));
|
||||
}
|
||||
|
||||
rsx! {
|
||||
@ -29,6 +46,7 @@ pub fn Blog(tag: String) -> Element
|
||||
div
|
||||
{
|
||||
class: "page_content",
|
||||
|
||||
BlogList
|
||||
{
|
||||
tags: categories
|
||||
|
||||
@ -13,6 +13,14 @@ const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||
#[component]
|
||||
pub fn Post(slug: String) -> Element
|
||||
{
|
||||
// Create a copy of the current slug to detect changes.
|
||||
let mut url_slug = use_signal(|| slug.clone());
|
||||
|
||||
if *url_slug.read() != slug
|
||||
{
|
||||
url_slug.set(slug.clone());
|
||||
}
|
||||
|
||||
rsx! {
|
||||
document::Stylesheet { href: BLOG_CSS }
|
||||
|
||||
@ -22,9 +30,10 @@ pub fn Post(slug: String) -> Element
|
||||
div
|
||||
{
|
||||
class: "page_content",
|
||||
|
||||
BlogPost
|
||||
{
|
||||
slug: slug
|
||||
slug: url_slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user