2025-09-24 19:50:38 -04:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
|
|
|
|
|
use dioxus::prelude::*;
|
|
|
|
|
|
|
|
|
|
use crate::togglable::Togglable;
|
|
|
|
|
use crate::page::Page;
|
|
|
|
|
use crate::server::*;
|
2025-09-28 20:25:19 -04:00
|
|
|
use crate::settings::{BardSettings};
|
2025-09-24 19:50:38 -04:00
|
|
|
use super::tags::*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
|
pub fn BlogItem(title: String, slug: String, author: String, summary: String,
|
|
|
|
|
tags: Vec<String>)
|
|
|
|
|
-> Element
|
|
|
|
|
{
|
2025-10-09 23:49:35 -04:00
|
|
|
let author_future = use_server_future(move ||
|
2025-10-07 11:31:07 -04:00
|
|
|
{
|
|
|
|
|
let handle: String = author.clone();
|
|
|
|
|
|
|
|
|
|
async move { get_author(handle).await }
|
2025-10-09 23:49:35 -04:00
|
|
|
})?;
|
2025-10-07 11:31:07 -04:00
|
|
|
|
2025-09-24 19:50:38 -04:00
|
|
|
rsx!
|
|
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
article
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
|
|
|
|
class: "blog_item",
|
|
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
h2
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
Link
|
|
|
|
|
{
|
|
|
|
|
to: Page::Post { slug: slug.clone() },
|
|
|
|
|
|
|
|
|
|
"{title}"
|
|
|
|
|
}
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TagList
|
|
|
|
|
{
|
|
|
|
|
for tag in tags
|
|
|
|
|
{
|
|
|
|
|
TagItem
|
|
|
|
|
{
|
|
|
|
|
tag: tag.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 11:31:07 -04:00
|
|
|
if let Some(Ok(Some(adventurer))) = (author_future.value())()
|
|
|
|
|
{
|
|
|
|
|
p
|
|
|
|
|
{
|
|
|
|
|
b
|
|
|
|
|
{
|
|
|
|
|
"Author: "
|
|
|
|
|
a
|
|
|
|
|
{
|
|
|
|
|
href: "{adventurer.legend.profile}", "{adventurer.name}"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
p { "Loading author..." }
|
|
|
|
|
}
|
2025-09-24 19:50:38 -04:00
|
|
|
|
|
|
|
|
p { "{summary}" }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
|
pub fn BlogList(tags: Signal<HashSet<String>>, children: Element) -> Element
|
|
|
|
|
{
|
2025-09-28 20:25:19 -04:00
|
|
|
// Retrieve the provided settings from context.
|
|
|
|
|
let settings = use_context::<BardSettings>();
|
|
|
|
|
|
2025-10-09 23:49:35 -04:00
|
|
|
let list = use_server_future(move ||
|
2025-09-27 13:21:56 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
let tags = tags();
|
|
|
|
|
let categories: Vec<String> = tags.iter().cloned().collect();
|
2025-09-24 19:50:38 -04:00
|
|
|
|
|
|
|
|
async move { get_blog_list(categories).await }
|
2025-10-09 23:49:35 -04:00
|
|
|
})?;
|
2025-09-24 19:50:38 -04:00
|
|
|
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
section
|
|
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
class: "blog_list",
|
|
|
|
|
|
2025-10-09 17:00:56 -04:00
|
|
|
match settings.blog_image
|
2025-09-28 20:25:19 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
Some(image) =>
|
2025-09-28 20:25:19 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
match settings.blog_name
|
2025-09-28 20:25:19 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
Some(title) =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
h1 { class: "blog_title visually_hidden", "{title}" }
|
|
|
|
|
img { class: "blog_title_image", alt: "{title} blog logo", src: image }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
h1 { class: "blog_title visually_hidden", "Blog" }
|
|
|
|
|
img { class: "blog_title_image", alt: "Blog logo", src: image }
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 20:25:19 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-09 17:00:56 -04:00
|
|
|
|
2025-09-28 20:25:19 -04:00
|
|
|
None =>
|
|
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
match settings.blog_name
|
2025-09-28 20:25:19 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
Some(title) =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
h1 { class: "blog_title", "{title}" }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
h1 { class: "blog_title visually_hidden", "Blog" }
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 20:25:19 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-24 19:50:38 -04:00
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
if let Some(Ok(lores)) = &*list.read()
|
|
|
|
|
{
|
|
|
|
|
for lore in lores
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
BlogItem
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
key: "{lore.slug}",
|
2025-09-27 13:21:56 -04:00
|
|
|
title: lore.title.clone(),
|
|
|
|
|
slug: lore.slug.clone(),
|
|
|
|
|
author: lore.author.clone(),
|
|
|
|
|
summary: lore.summary.clone(),
|
|
|
|
|
tags: lore.tags.clone()
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 20:25:19 -04:00
|
|
|
else if let Some(Err(_e)) = &*list.read()
|
2025-09-27 13:21:56 -04:00
|
|
|
{
|
2025-09-28 20:25:19 -04:00
|
|
|
p { "Please choose a category to see related blog posts." }
|
2025-09-27 13:21:56 -04:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
p { "Loading..." }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{children}
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
|
2025-09-24 19:50:38 -04:00
|
|
|
#[component]
|
|
|
|
|
pub fn ToggleTag(tag: String, toggled_tags: Signal<HashSet<String>>) -> Element
|
|
|
|
|
{
|
2025-10-09 17:00:56 -04:00
|
|
|
let is_checked = toggled_tags.read().is_toggled(&tag);
|
2025-09-24 19:50:38 -04:00
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
label
|
|
|
|
|
{
|
|
|
|
|
class: "toggle_button",
|
|
|
|
|
|
|
|
|
|
input
|
|
|
|
|
{
|
|
|
|
|
r#type: "checkbox",
|
2025-10-09 17:00:56 -04:00
|
|
|
checked: is_checked,
|
2025-09-24 19:50:38 -04:00
|
|
|
onchange: move |_|
|
|
|
|
|
{
|
|
|
|
|
toggled_tags.write().toggle(&tag.clone());
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
"{tag}"
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
|
2025-10-10 00:01:31 -04:00
|
|
|
// 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.
|
2025-09-24 19:50:38 -04:00
|
|
|
#[component]
|
2025-10-08 15:17:48 -04:00
|
|
|
pub fn TagSelector(url_tag: ReadSignal<String>,
|
2025-09-27 13:21:56 -04:00
|
|
|
toggled_tags: Signal<HashSet<String>>) -> Element
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
// Use use_resource to handle both fetching tags AND initializing selection
|
2025-10-10 00:01:31 -04:00
|
|
|
let tags_and_selection = use_resource(move ||
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
let current_url = url_tag();
|
|
|
|
|
|
|
|
|
|
async move
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
match get_tags().await
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
Ok(available_tags) =>
|
|
|
|
|
{
|
|
|
|
|
// 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(¤t_url);
|
|
|
|
|
|
|
|
|
|
let filtered: HashSet<String> = url_categories.into_iter()
|
|
|
|
|
.filter(|tag| available_tags.contains(tag))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
filtered
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok((available_tags, selected_tags))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Err(e) =>
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
eprintln!("RESOURCE - Error: {}", e);
|
|
|
|
|
Err(e)
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-10 00:01:31 -04:00
|
|
|
});
|
2025-09-24 19:50:38 -04:00
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
// 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());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2025-09-24 19:50:38 -04:00
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
section
|
|
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
class: "blog_nav",
|
|
|
|
|
|
|
|
|
|
h2 { class: "visually_hidden", "Filters and Navigation" }
|
|
|
|
|
|
|
|
|
|
fieldset
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
|
|
|
|
class: "tag_style",
|
2025-09-27 13:21:56 -04:00
|
|
|
|
|
|
|
|
legend { "Category Filter" }
|
|
|
|
|
|
2025-09-24 19:50:38 -04:00
|
|
|
ul
|
|
|
|
|
{
|
|
|
|
|
class: "tag_list",
|
|
|
|
|
|
2025-09-27 13:21:56 -04:00
|
|
|
match &*tags_and_selection.read()
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
Some(Ok((available_tags, _))) =>
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
rsx!
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
for tag in available_tags
|
2025-09-24 19:50:38 -04:00
|
|
|
{
|
2025-09-27 13:21:56 -04:00
|
|
|
li
|
|
|
|
|
{
|
|
|
|
|
key: "selector-{tag}",
|
|
|
|
|
class: "tag_item",
|
|
|
|
|
|
|
|
|
|
ToggleTag
|
|
|
|
|
{
|
|
|
|
|
tag: tag.clone(),
|
|
|
|
|
toggled_tags: toggled_tags
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-27 13:21:56 -04:00
|
|
|
|
|
|
|
|
Some(Err(_)) =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
li { "Error loading tags" }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None =>
|
|
|
|
|
{
|
|
|
|
|
rsx!
|
|
|
|
|
{
|
|
|
|
|
li { "Loading tags..." }
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-24 19:50:38 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-27 13:21:56 -04:00
|
|
|
|
|
|
|
|
fn convert_categories(categories: &str) -> HashSet<String>
|
|
|
|
|
{
|
|
|
|
|
categories
|
|
|
|
|
.split('+')
|
|
|
|
|
.filter(|s| !s.is_empty() && *s != "all")
|
|
|
|
|
.map(str::to_string)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|