Adding and moving a lot of components.
This is the main thrust of the library. I know it is a lot of changes, but I was running out of time and had to hammer them all in.
This commit is contained in:
@ -9,11 +9,17 @@ authors = ["CyberMages LLC <Software@CyberMagesLLC.com>",
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "blog"
|
||||||
|
path = "examples/blog.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { version = "*", features = ["router", "fullstack"], optional = true }
|
dioxus = { version = "*", features = ["router", "fullstack"] }
|
||||||
tavern = { version = "0.2.9", path = "../tavern", registry = "cybermages", optional = true}
|
tavern = { version = "0.2.9", path = "../tavern", optional = true}
|
||||||
tokio = { version = "1.0", features = ["full"], optional = true }
|
tokio = { version = "1.0", features = ["rt", "macros"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tavern", "dioxus/web"]
|
default = ["web"]
|
||||||
|
web = ["tavern", "dioxus/web"]
|
||||||
server = ["tavern/database", "dioxus/server", "tokio"]
|
server = ["tavern/database", "dioxus/server", "tokio"]
|
||||||
|
|||||||
@ -1,496 +0,0 @@
|
|||||||
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..." }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
194
bard/src/components/list.rs
Normal file
194
bard/src/components/list.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||||
|
|
||||||
|
use crate::togglable::Togglable;
|
||||||
|
use crate::page::Page;
|
||||||
|
use crate::server::*;
|
||||||
|
use super::tags::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BlogAuthor() -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
section
|
||||||
|
{
|
||||||
|
class: "blog_author_style",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
||||||
|
tags: Vec<String>)
|
||||||
|
-> Element
|
||||||
|
{
|
||||||
|
println!("Blog Item: {title} -- [{tags:?}]");
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
li
|
||||||
|
{
|
||||||
|
key: "{slug}",
|
||||||
|
class: "blog_item",
|
||||||
|
|
||||||
|
Link
|
||||||
|
{
|
||||||
|
to: Page::Post { slug: slug.clone() },
|
||||||
|
h1 { "{title}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
TagList
|
||||||
|
{
|
||||||
|
for tag in tags
|
||||||
|
{
|
||||||
|
TagItem
|
||||||
|
{
|
||||||
|
tag: tag.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 }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
section
|
||||||
|
{
|
||||||
|
class: "blog_list_style",
|
||||||
|
ul
|
||||||
|
{
|
||||||
|
class: "blog_list",
|
||||||
|
|
||||||
|
if let Some(Ok(lores)) = &*list.read()
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if let Some(Err(e)) = &*list.read()
|
||||||
|
{
|
||||||
|
p { "Unable to show post header." }
|
||||||
|
p { "{e}" }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p { "Loading..." }
|
||||||
|
}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
label
|
||||||
|
{
|
||||||
|
class: "toggle_button",
|
||||||
|
|
||||||
|
input
|
||||||
|
{
|
||||||
|
r#type: "checkbox",
|
||||||
|
checked: toggled_tags.read().is_toggled(&tag),
|
||||||
|
onchange: move |_|
|
||||||
|
{
|
||||||
|
toggled_tags.write().toggle(&tag.clone());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
span { "{tag}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagSelector(show_all: Signal<bool>, toggled_tags: Signal<HashSet<String>>) -> Element
|
||||||
|
{
|
||||||
|
let toggle_all: bool = show_all();
|
||||||
|
|
||||||
|
println!("Tag Selector toggled tags: {:?}", toggled_tags());
|
||||||
|
let tags_future = use_server_future(move || async move { get_tags().await })?;
|
||||||
|
|
||||||
|
use_effect(move ||
|
||||||
|
{
|
||||||
|
if let Some(Ok(tags)) = &*tags_future.read()
|
||||||
|
{
|
||||||
|
if toggle_all
|
||||||
|
{
|
||||||
|
for tag in tags
|
||||||
|
{
|
||||||
|
toggled_tags.write().insert(tag.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
section
|
||||||
|
{
|
||||||
|
class: "blog_nav_style",
|
||||||
|
div
|
||||||
|
{
|
||||||
|
class: "tag_style",
|
||||||
|
h2 { "Categories" }
|
||||||
|
ul
|
||||||
|
{
|
||||||
|
class: "tag_list",
|
||||||
|
|
||||||
|
if let Some(Ok(tags)) = tags_future()
|
||||||
|
{
|
||||||
|
for tag in tags
|
||||||
|
{
|
||||||
|
li
|
||||||
|
{
|
||||||
|
key: "selector-{tag}",
|
||||||
|
class: "tag_item",
|
||||||
|
|
||||||
|
ToggleTag
|
||||||
|
{
|
||||||
|
tag: &tag,
|
||||||
|
toggled_tags: toggled_tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
bard/src/components/mod.rs
Normal file
8
bard/src/components/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mod list;
|
||||||
|
mod post;
|
||||||
|
mod tags;
|
||||||
|
|
||||||
|
|
||||||
|
pub use self::list::*;
|
||||||
|
pub use self::post::*;
|
||||||
|
pub use self::tags::*;
|
||||||
180
bard/src/components/post.rs
Normal file
180
bard/src/components/post.rs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use tavern::{Adventurer, Legend, Lore, Tale};
|
||||||
|
|
||||||
|
use crate::togglable::Togglable;
|
||||||
|
use crate::page::Page;
|
||||||
|
use crate::server::*;
|
||||||
|
use super::tags::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BlogAuthor() -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
section
|
||||||
|
{
|
||||||
|
class: "blog_author_style",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn PostHeaderAuthor(adventurer: Adventurer) -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
h4
|
||||||
|
{
|
||||||
|
"Author: ",
|
||||||
|
a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn PostHeader(tale: Tale) -> Element
|
||||||
|
{
|
||||||
|
let author_future = use_server_future(move ||
|
||||||
|
{
|
||||||
|
let handle: String = tale.lore.author.clone();
|
||||||
|
|
||||||
|
async move { get_author(handle).await }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
h1 { {tale.lore.title} }
|
||||||
|
|
||||||
|
TagList
|
||||||
|
{
|
||||||
|
for tag in tale.lore.tags
|
||||||
|
{
|
||||||
|
TagItem
|
||||||
|
{
|
||||||
|
tag: tag.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Ok(Some(adventurer))) = (author_future.value())()
|
||||||
|
{
|
||||||
|
PostHeaderAuthor { adventurer: adventurer }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p { "Loading author..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Story(text: String) -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
div { dangerous_inner_html: "{text}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
|
||||||
|
{
|
||||||
|
// Run a once off server fetch of the unchanging blog data.
|
||||||
|
let post_future = 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 }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
article
|
||||||
|
{
|
||||||
|
class: "blog_post_style",
|
||||||
|
|
||||||
|
if let Some(Ok(tale)) = (post_future.value())()
|
||||||
|
{
|
||||||
|
PostHeader
|
||||||
|
{
|
||||||
|
tale: tale.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
Story { text: tale.story }
|
||||||
|
|
||||||
|
|
||||||
|
//Author {}
|
||||||
|
}
|
||||||
|
else if let Some(Err(e)) = (post_future.value())()
|
||||||
|
{
|
||||||
|
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!
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
key: "{tag}",
|
||||||
|
class: "tag_item",
|
||||||
|
Link
|
||||||
|
{
|
||||||
|
to: Page::Blog { tag: tag.clone() },
|
||||||
|
|
||||||
|
"{tag}"
|
||||||
|
}
|
||||||
|
|
||||||
|
//a { href: "/blog/{tag}", "{tag}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if let Some(Err(e)) = &*tags.read()
|
||||||
|
{
|
||||||
|
p { "Unable to show desired post." }
|
||||||
|
p { "{e}" }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p { "Loading..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
bard/src/components/tags.rs
Normal file
51
bard/src/components/tags.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::page::Page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagItem(tag: String, style: Option<String>) -> Element
|
||||||
|
{
|
||||||
|
let tag_style: String = match style
|
||||||
|
{
|
||||||
|
None => { String::new() }
|
||||||
|
Some(s) => { s }
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
li
|
||||||
|
{
|
||||||
|
key: "{tag}",
|
||||||
|
class: "tag_item",
|
||||||
|
|
||||||
|
Link
|
||||||
|
{
|
||||||
|
class: "{tag_style}",
|
||||||
|
|
||||||
|
to: Page::Blog { tag: tag.clone() },
|
||||||
|
|
||||||
|
"{tag}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagList(children: Element) -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
h5
|
||||||
|
{
|
||||||
|
class: "blog_tag_style",
|
||||||
|
|
||||||
|
ul
|
||||||
|
{
|
||||||
|
class: "tag_list",
|
||||||
|
{children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ mod components;
|
|||||||
mod page;
|
mod page;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod togglable;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,35 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::pages::{Blog, Post};
|
use crate::pages::{Blog, Post, Root};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn BlogLayout() -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
document::Stylesheet { href: BLOG_CSS }
|
||||||
|
|
||||||
|
Outlet::<Page> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//#[derive(Debug, Clone, Routable, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub enum Page
|
pub enum Page
|
||||||
{
|
{
|
||||||
|
#[layout(BlogLayout)]
|
||||||
|
#[route("/")]
|
||||||
|
Root { },
|
||||||
|
|
||||||
#[route("/:tag")]
|
#[route("/:tag")]
|
||||||
Blog { tag: String },
|
Blog { tag: String },
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
mod blog;
|
|
||||||
mod post;
|
|
||||||
|
|
||||||
|
|
||||||
pub use crate::pages::blog::Blog;
|
|
||||||
pub use crate::pages::post::Post;
|
|
||||||
@ -7,39 +7,37 @@ use crate::page::Page;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn convert_categories(categories: &str) -> HashSet<String>
|
fn convert_categories(categories: &str) -> HashSet<String>
|
||||||
{
|
{
|
||||||
if categories.is_empty() || categories == "all"
|
categories
|
||||||
{
|
.split('+')
|
||||||
HashSet::new()
|
.filter(|s| !s.is_empty() && *s != "all")
|
||||||
}
|
.map(str::to_string)
|
||||||
else
|
.collect()
|
||||||
{
|
|
||||||
categories.split('+').map(|s| s.to_string()).collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Blog page
|
/// Blog page
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Blog(tag: String) -> Element
|
pub fn Blog(tag: ReadOnlySignal<String>) -> Element
|
||||||
{
|
{
|
||||||
|
let mut show_all: Signal<bool> =
|
||||||
|
use_signal(|| tag().is_empty() || tag() == "all");
|
||||||
|
|
||||||
let mut categories: Signal<HashSet<String>> =
|
let mut categories: Signal<HashSet<String>> =
|
||||||
use_signal(|| convert_categories(&tag));
|
use_signal(|| convert_categories(&tag()));
|
||||||
|
|
||||||
if *categories.read() != convert_categories(&tag)
|
use_effect(move ||
|
||||||
{
|
{
|
||||||
categories.set(convert_categories(&tag));
|
let new_tags = convert_categories(&tag());
|
||||||
}
|
categories.set(new_tags);
|
||||||
|
});
|
||||||
|
|
||||||
rsx! {
|
println!("Blog Categories: {:?}", categories());
|
||||||
document::Stylesheet { href: BLOG_CSS }
|
|
||||||
|
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
main
|
main
|
||||||
{
|
{
|
||||||
class: "blog_style",
|
class: "blog_style",
|
||||||
@ -49,12 +47,13 @@ pub fn Blog(tag: String) -> Element
|
|||||||
|
|
||||||
BlogList
|
BlogList
|
||||||
{
|
{
|
||||||
tags: categories
|
tags: categories.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
TagSelector
|
TagSelector
|
||||||
{
|
{
|
||||||
toggled_tags: categories
|
show_all: show_all.clone(),
|
||||||
|
toggled_tags: categories.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
bard/src/pages/mod.rs
Normal file
8
bard/src/pages/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mod blog;
|
||||||
|
mod post;
|
||||||
|
mod root;
|
||||||
|
|
||||||
|
|
||||||
|
pub use self::blog::Blog;
|
||||||
|
pub use self::post::Post;
|
||||||
|
pub use self::root::Root;
|
||||||
@ -5,25 +5,15 @@ use crate::page::Page;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const BLOG_CSS: Asset = asset!("/assets/css/blog.css");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Blog page
|
/// Blog page
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Post(slug: String) -> Element
|
pub fn Post(slug: ReadOnlySignal<String>) -> Element
|
||||||
{
|
{
|
||||||
// Create a copy of the current slug to detect changes.
|
// Create a copy of the current slug to detect changes.
|
||||||
let mut url_slug = use_signal(|| slug.clone());
|
let mut url_slug = use_signal(|| slug());
|
||||||
|
|
||||||
if *url_slug.read() != slug
|
rsx!
|
||||||
{
|
{
|
||||||
url_slug.set(slug.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
document::Stylesheet { href: BLOG_CSS }
|
|
||||||
|
|
||||||
main
|
main
|
||||||
{
|
{
|
||||||
class: "blog_style",
|
class: "blog_style",
|
||||||
|
|||||||
21
bard/src/pages/root.rs
Normal file
21
bard/src/pages/root.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::page::Page;
|
||||||
|
use crate::pages::Blog;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Blog page
|
||||||
|
#[component]
|
||||||
|
pub fn Root() -> Element
|
||||||
|
{
|
||||||
|
rsx!
|
||||||
|
{
|
||||||
|
Blog
|
||||||
|
{
|
||||||
|
tag: String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -96,13 +96,11 @@ pub async fn get_blog_post(slug: String) -> Result<Tale, ServerFnError>
|
|||||||
|
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_author(handle: String) -> Result<Adventurer, ServerFnError>
|
pub async fn get_author(handle: String) -> Result<Option<Adventurer>, ServerFnError>
|
||||||
{
|
{
|
||||||
let db = get_database().await?;
|
let db = get_database().await?;
|
||||||
|
|
||||||
let author = db.get_adventurer(&handle)
|
let author = db.get_adventurer(&handle)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ServerFnError::new(e))?;
|
.map_err(|e| ServerFnError::new(e))?;
|
||||||
|
Ok(author)
|
||||||
author.ok_or(ServerFnError::new(format!("Author {} not found.", handle)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
bard/src/togglable.rs
Normal file
37
bard/src/togglable.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user