Files
tavernworks/bard/src/components.rs

497 lines
10 KiB
Rust
Raw Normal View History

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<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>
{
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<String>)
-> 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<HashSet<String>>, 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<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 }
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<HashSet<String>>) -> Element
{
let mut categories = use_signal(|| Vec::<String>::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<Option<Adventurer>>) -> Element
{
rsx! {
if let Some(author) = &*adventurer.read()
{
h4
{
"Author: ",
a { href: "{author.legend.profile}", "{author.name} @{author.handle}" }
}
}
else
{
h4 { "Author: Unknown" }
}
}
}
#[component]
pub fn PostHeader(tale: Signal<Option<Tale>>,
adventurer: Signal<Option<Adventurer>>) -> Element
{
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!
{
if let Some(tale) = tale()
{
h1 { {tale.lore.title} }
TagList
{
for (tag, style) in converted_tags().iter()
{
TagItem
{
tag: tag.clone(),
style: style.clone()
}
}
}
}
else
{
p { "Loading post header..." }
}
if let Some(_) = adventurer()
{
PostHeaderAuthor { adventurer: adventurer }
}
else
{
p { "Loading author..." }
}
}
}
#[component]
pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
{
// The tale depends on the post future resolving.
let mut tale: Signal<Option<Tale>> = 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.read()
{
Some(data) => data.lore.author.to_owned(),
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",
if let Some(Ok(post)) = &*post.read()
{
PostHeader
{
tale: tale,
adventurer: adventurer
}
//Story {}
div { dangerous_inner_html: "{post.story}" }
//Author {}
}
else if let Some(Err(e)) = &*post.read()
{
p { "Unable to load desired post: {slug.read()}" }
p { "{e}" }
}
else
{
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",
if let Some(Ok(categories)) = &*tags.read()
{
for tag in categories
{
li
{
class: "tag_item",
a { href: "/blog/{strip_tag(tag)}", "{strip_tag(tag)}" }
}
}
}
else if let Some(Err(e)) = &*tags.read()
{
p { "Unable to show desired post." }
p { "{e}" }
}
else
{
p { "Loading..." }
}
}
}
}
}
}