Compare commits

...

2 Commits

Author SHA1 Message Date
3f4440ec2f Switching to a single stylesheet.
Also bumped the bard version.
2025-09-27 13:29:08 -04:00
e7969c8050 Fixed the TagSelector reactivity.
use_server_future was not reliably re-running when url_tag changed during
direct URL navigation, causing tags to remain unselected. use_resource
provides consistent reactivity across all navigation methods and handles
both data fetching and selection logic atomically.
2025-09-27 13:21:56 -04:00
12 changed files with 363 additions and 676 deletions

8
Cargo.lock generated
View File

@ -224,7 +224,7 @@ dependencies = [
[[package]] [[package]]
name = "bard" name = "bard"
version = "0.0.23" version = "0.3.1"
dependencies = [ dependencies = [
"dioxus", "dioxus",
"tavern", "tavern",
@ -263,7 +263,7 @@ dependencies = [
[[package]] [[package]]
name = "blog_test" name = "blog_test"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-server", "axum-server",
@ -1941,7 +1941,7 @@ checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
[[package]] [[package]]
name = "loreweaver" name = "loreweaver"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"clap", "clap",
"tavern", "tavern",
@ -3104,7 +3104,7 @@ dependencies = [
[[package]] [[package]]
name = "tavern" name = "tavern"
version = "0.2.9" version = "0.3.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"pulldown-cmark", "pulldown-cmark",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bard" name = "bard"
version = "0.3.0" version = "0.3.1"
edition = "2024" edition = "2024"
description = "Dioxus components that will display a Tavern blogging system Blog." description = "Dioxus components that will display a Tavern blogging system Blog."
repository = "/CyberMages/tavern" repository = "/CyberMages/tavern"

View File

@ -1,3 +1,16 @@
.visually_hidden
{
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
border: 0 !important;
white-space: nowrap !important;
}
.blog_style .blog_style
{ {
display: flex; display: flex;
@ -17,3 +30,165 @@
container-type: inline-size; container-type: inline-size;
} }
} }
.blog_list
{
order: 5;
.blog_title
{
margin-top: 0px;
}
.blog_item
{
margin-bottom: 30px;
h2
{
margin: 0px;
color: var(--accent-color);
}
p
{
margin: 5px 0px 0px 0px;
display: block;
}
}
}
.blog_post
{
order: 5;
h1
{
margin: 0px;
color: var(--accent-color);
}
h4
{
margin-top: 10px;
margin-bottom: 30px;
}
li
{
margin-bottom: 5px;
}
p
{
margin-top: 0px;
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);
}
}
}
/* Tags will have a class of "tag_{tag_name}" that allows specific
* customization.
*
* .tag_easy
* {
* background-color: #248721;
* }
*
* */
.blog_tag
{
display: flex;
flex-direction: row;
.tag_list
{
display: flex;
flex-direction: row;
}
}
.blog_nav
{
order: 1;
display: flex;
flex-direction: column;
margin-top: 70px;
margin-right: 70px;
fieldset
{
border: none;
padding: 0;
margin: 0;
min-inline-size: unset;
}
legend
{
min-width: 120px;
padding: 0;
margin: 0;
}
.tag_list
{
display: flex;
flex-direction: column;
}
@container site (max-width: 1230px)
{
display: none;
}
}
.tag_list
{
list-style: none;
padding: 0px;
margin: 0px;
.tag_item
{
margin-top: 5px;
margin-right: 5px;
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);
}
}
}
}

View File

@ -1,230 +0,0 @@
.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);
}
}
}

View File

@ -1,27 +0,0 @@
.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;
}
}
}
}

View File

@ -1,117 +0,0 @@
.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;
order: 5;
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);
}
}
}
}
}
}

View File

@ -1,119 +0,0 @@
.blog_post_style
{
order: 1;
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);
}
}
}

View File

@ -1,54 +0,0 @@
.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;
}
}

View File

@ -30,18 +30,20 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
tags: Vec<String>) tags: Vec<String>)
-> Element -> Element
{ {
println!("Blog Item: {title} -- [{tags:?}]");
rsx! rsx!
{ {
li article
{ {
key: "{slug}",
class: "blog_item", class: "blog_item",
Link h2
{ {
to: Page::Post { slug: slug.clone() }, Link
h1 { "{title}" } {
to: Page::Post { slug: slug.clone() },
"{title}"
}
} }
TagList TagList
@ -55,7 +57,7 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
} }
} }
h4 { "Author: {author}" } p { b { "Author: {author}" } }
p { "{summary}" } p { "{summary}" }
} }
@ -66,8 +68,10 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String,
#[component] #[component]
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
{ {
let list = use_server_future(move || { let list = use_server_future(move ||
let categories = tags().iter().cloned().collect(); {
let t = tags();
let categories = t.iter().cloned().collect();
async move { get_blog_list(categories).await } async move { get_blog_list(categories).await }
})?; })?;
@ -76,41 +80,40 @@ pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
{ {
section section
{ {
class: "blog_list_style", class: "blog_list",
ul
{
class: "blog_list",
if let Some(Ok(lores)) = &*list.read() h1 { class: "blog_title", "Runes & Ramblings" }
if let Some(Ok(lores)) = &*list.read()
{
for lore in lores
{ {
for lore in lores BlogItem
{ {
BlogItem title: lore.title.clone(),
{ slug: lore.slug.clone(),
title: lore.title.clone(), author: lore.author.clone(),
slug: lore.slug.clone(), summary: lore.summary.clone(),
author: lore.author.clone(), tags: lore.tags.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}
} }
else if let Some(Err(e)) = &*list.read()
{
p { "Unable to show post header." }
p { "{e}" }
}
else
{
p { "Loading..." }
}
{children}
} }
} }
} }
#[component] #[component]
pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
{ {
@ -130,65 +133,151 @@ pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
}, },
} }
span { "{tag}" } "{tag}"
} }
} }
} }
// Using use_resource instead of use_server_future for URL-dependent tag selection
//
// While use_server_future should theoretically be reactive when reading signals in the
// closure (per Dioxus docs), in practice it doesn't reliably re-run when the url_tag
// signal changes, especially during direct URL navigation (typing URLs in browser).
//
// use_resource provides more reliable reactivity for this use case because:
// 1. It explicitly depends on url_tag and consistently re-runs when it changes
// 2. It handles both async data fetching AND selection logic in a single atomic operation
// 3. It works consistently across all navigation methods (links, direct URLs, etc.)
// 4. It avoids timing coordination issues between separate hooks
//
// This approach combines fetching available tags from the server with determining
// which tags should be selected based on the current URL, returning both pieces
// of data together for clean state management.
#[component] #[component]
pub fn TagSelector(show_all: Signal<bool>, toggled_tags: Signal<HashSet<String>>) -> Element pub fn TagSelector(url_tag: ReadOnlySignal<String>,
toggled_tags: Signal<HashSet<String>>) -> Element
{ {
let toggle_all: bool = show_all(); // Use use_resource to handle both fetching tags AND initializing selection
let tags_and_selection = use_resource(move ||
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() let current_url = url_tag();
async move
{ {
if toggle_all match get_tags().await
{ {
for tag in tags Ok(available_tags) =>
{ {
toggled_tags.write().insert(tag.clone()); // Determine what should be selected based on URL
let should_show_all = current_url.is_empty() || current_url == "all";
let selected_tags: HashSet<String> = if should_show_all
{
available_tags.iter().cloned().collect()
}
else
{
let url_categories = convert_categories(&current_url);
let filtered: HashSet<String> = url_categories.into_iter()
.filter(|tag| available_tags.contains(tag))
.collect();
filtered
};
Ok((available_tags, selected_tags))
}
Err(e) =>
{
eprintln!("RESOURCE - Error: {}", e);
Err(e)
} }
} }
} }
}); });
// Separate effect to update toggled_tags when resource completes
// This separates reading the resource from writing to toggled_tags
use_effect(move ||
{
if let Some(Ok((_, selected_tags))) = &*tags_and_selection.read()
{
toggled_tags.set(selected_tags.clone());
}
});
rsx! rsx!
{ {
section section
{ {
class: "blog_nav_style", class: "blog_nav",
div
h2 { class: "visually_hidden", "Filters and Navigation" }
fieldset
{ {
class: "tag_style", class: "tag_style",
h2 { "Categories" }
legend { "Category Filter" }
ul ul
{ {
class: "tag_list", class: "tag_list",
if let Some(Ok(tags)) = tags_future() match &*tags_and_selection.read()
{ {
for tag in tags Some(Ok((available_tags, _))) =>
{ {
li rsx!
{ {
key: "selector-{tag}", for tag in available_tags
class: "tag_item",
ToggleTag
{ {
tag: &tag, li
toggled_tags: toggled_tags {
key: "selector-{tag}",
class: "tag_item",
ToggleTag
{
tag: tag.clone(),
toggled_tags: toggled_tags
}
}
} }
} }
} }
Some(Err(_)) =>
{
rsx!
{
li { "Error loading tags" }
}
}
None =>
{
rsx!
{
li { "Loading tags..." }
}
}
} }
} }
} }
} }
} }
} }
fn convert_categories(categories: &str) -> HashSet<String>
{
categories
.split('+')
.filter(|s| !s.is_empty() && *s != "all")
.map(str::to_string)
.collect()
}

View File

@ -30,10 +30,13 @@ pub fn PostHeaderAuthor(adventurer: Adventurer) -> Element
{ {
rsx! rsx!
{ {
h4 p
{ {
"Author: ", b
a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" } {
"Author: ",
a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" }
}
} }
} }
} }
@ -99,7 +102,7 @@ pub fn BlogPost(slug: Signal<String>, children: Element) -> Element
{ {
article article
{ {
class: "blog_post_style", class: "blog_post",
if let Some(Ok(tale)) = (post_future.value())() if let Some(Ok(tale)) = (post_future.value())()
{ {
@ -137,42 +140,36 @@ pub fn TagNav() -> Element
{ {
section section
{ {
class: "blog_nav_style", class: "blog_nav",
div
fieldset
{ {
class: "tag_style", class: "tag_list",
h2 { "Categories" }
ul legend { "Category Filter" }
if let Some(Ok(categories)) = &*tags.read()
{ {
class: "tag_list", for tag in categories
if let Some(Ok(categories)) = &*tags.read()
{ {
for tag in categories Link
{
li
{ {
key: "{tag}", class: "tag_item tag_{tag}",
class: "tag_item",
Link
{
to: Page::Blog { tag: tag.clone() },
"{tag}" to: Page::Blog { tag: tag.clone() },
}
//a { href: "/blog/{tag}", "{tag}" } "{tag}"
} }
} }
} }
else if let Some(Err(e)) = &*tags.read() else if let Some(Err(e)) = &*tags.read()
{ {
p { "Unable to show desired post." } p { "Unable to load tags." }
p { "{e}" } p { "{e}" }
} }
else else
{ {
p { "Loading..." } p { "Loading..." }
}
} }
} }
} }

View File

@ -37,15 +37,10 @@ pub fn TagList(children: Element) -> Element
{ {
rsx! rsx!
{ {
h5 ul
{ {
class: "blog_tag_style", class: "blog_tag tag_list",
{children}
ul
{
class: "tag_list",
{children}
}
} }
} }
} }

View File

@ -7,34 +7,12 @@ use crate::page::Page;
fn convert_categories(categories: &str) -> HashSet<String>
{
categories
.split('+')
.filter(|s| !s.is_empty() && *s != "all")
.map(str::to_string)
.collect()
}
/// Blog page /// Blog page
#[component] #[component]
pub fn Blog(tag: ReadOnlySignal<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(|| HashSet::new());
use_effect(move ||
{
let new_tags = convert_categories(&tag());
categories.set(new_tags);
});
println!("Blog Categories: {:?}", categories());
rsx! rsx!
{ {
@ -47,13 +25,13 @@ pub fn Blog(tag: ReadOnlySignal<String>) -> Element
BlogList BlogList
{ {
tags: categories.clone() tags: categories
} }
TagSelector TagSelector
{ {
show_all: show_all.clone(), url_tag: tag,
toggled_tags: categories.clone() toggled_tags: categories
} }
} }
} }