use std::collections::HashSet; use dioxus::prelude::*; use crate::togglable::Togglable; use crate::page::Page; use crate::server::*; use crate::settings::{BardSettings}; use super::tags::*; #[component] pub fn BlogItem(title: String, slug: String, author: String, summary: String, tags: Vec) -> Element { let author_future = use_server_future(move || { let handle: String = author.clone(); async move { get_author(handle).await } })?; rsx! { article { class: "blog_item", h2 { Link { to: Page::Post { slug: slug.clone() }, "{title}" } } TagList { for tag in tags { TagItem { tag: tag.clone(), } } } if let Some(Ok(Some(adventurer))) = (author_future.value())() { p { b { "Author: " a { href: "{adventurer.legend.profile}", "{adventurer.name}" } } } } else { p { "Loading author..." } } p { "{summary}" } } } } #[component] pub fn BlogList(tags: Signal>, children: Element) -> Element { // Retrieve the provided settings from context. let settings = use_context::(); let list = use_server_future(move || { let tags = tags(); let categories: Vec = tags.iter().cloned().collect(); async move { get_blog_list(categories).await } })?; rsx! { section { class: "blog_list", match settings.blog_image { Some(image) => { match settings.blog_name { 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 } } } } } None => { match settings.blog_name { Some(title) => { rsx! { h1 { class: "blog_title", "{title}" } } } None => { rsx! { h1 { class: "blog_title visually_hidden", "Blog" } } } } } } if let Some(Ok(lores)) = &*list.read() { for lore in lores { BlogItem { key: "{lore.slug}", 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 { "Please choose a category to see related blog posts." } } else { p { "Loading..." } } {children} } } } #[component] pub fn ToggleTag(tag: String, toggled_tags: Signal>) -> Element { let is_checked = toggled_tags.read().is_toggled(&tag); rsx! { label { class: "toggle_button", input { r#type: "checkbox", checked: is_checked, 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: ReadSignal, 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() }