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) -> Element { rsx! { article { class: "blog_item", h2 { Link { to: Page::Post { slug: slug.clone() }, "{title}" } } TagList { for tag in tags { TagItem { tag: tag.clone(), } } } p { b { "Author: {author}" } } p { "{summary}" } } } } #[component] pub fn BlogList(tags: Signal>, children: Element) -> Element { let list = use_server_future(move || { let t = tags(); let categories = t.iter().cloned().collect(); async move { get_blog_list(categories).await } })?; rsx! { section { class: "blog_list", h1 { class: "blog_title", "Runes & Ramblings" } 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>) -> 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()); }, } "{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] pub fn TagSelector(url_tag: ReadOnlySignal, toggled_tags: Signal>) -> Element { // Use use_resource to handle both fetching tags AND initializing selection let tags_and_selection = use_resource(move || { let current_url = url_tag(); async move { match get_tags().await { 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 = if should_show_all { available_tags.iter().cloned().collect() } else { let url_categories = convert_categories(¤t_url); let filtered: HashSet = 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! { section { class: "blog_nav", h2 { class: "visually_hidden", "Filters and Navigation" } fieldset { class: "tag_style", legend { "Category Filter" } ul { class: "tag_list", match &*tags_and_selection.read() { Some(Ok((available_tags, _))) => { rsx! { for tag in available_tags { li { 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 { categories .split('+') .filter(|s| !s.is_empty() && *s != "all") .map(str::to_string) .collect() }