You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

134 lines
4.5 KiB
Rust

//! Browse page — paginated quote list with author and tag filter controls.
use crate::api;
use crate::components::error::ErrorDisplay;
use crate::components::pagination::Pagination;
use crate::components::quote_card::QuoteCard;
use quotesdb::Quote;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
/// Browse page component.
///
/// Displays a paginated list of quotes. Supports filtering by author name
/// and tag. Fetches from the API whenever page, author, or tag state changes.
#[function_component(BrowsePage)]
pub fn browse_page() -> Html {
let page = use_state(|| 1u32);
let total_pages = use_state(|| 1u32);
let author_filter = use_state(String::new);
let tag_filter = use_state(String::new);
let quotes: UseStateHandle<Vec<Quote>> = use_state(Vec::new);
let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true);
// Fetch quotes whenever page, author, or tag changes
{
let page = page.clone();
let total_pages = total_pages.clone();
let author_filter = author_filter.clone();
let tag_filter = tag_filter.clone();
let quotes = quotes.clone();
let error = error.clone();
let loading = loading.clone();
let page_val = *page;
let author_val = (*author_filter).clone();
let tag_val = (*tag_filter).clone();
use_effect_with((page_val, author_val.clone(), tag_val.clone()), move |_| {
loading.set(true);
error.set(None);
let author = if author_val.is_empty() {
None
} else {
Some(author_val.clone())
};
let tag = if tag_val.is_empty() {
None
} else {
Some(tag_val.clone())
};
spawn_local(async move {
match api::list_quotes(page_val, author.as_deref(), tag.as_deref()).await {
Ok(resp) => {
quotes.set(resp.quotes);
total_pages.set(resp.total_pages.max(1));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
let on_page = {
let page = page.clone();
Callback::from(move |p: u32| page.set(p))
};
let on_author_input = {
let author_filter = author_filter.clone();
let page = page.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
author_filter.set(input.value());
page.set(1);
})
};
let on_tag_input = {
let tag_filter = tag_filter.clone();
let page = page.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
tag_filter.set(input.value());
page.set(1);
})
};
html! {
<div class="page-browse">
<h1 class="page-browse__title">{ "Browse Quotes" }</h1>
<div class="page-browse__filters">
<input
class="page-browse__filter-input"
type="text"
placeholder="Filter by author..."
value={(*author_filter).clone()}
oninput={on_author_input}
/>
<input
class="page-browse__filter-input"
type="text"
placeholder="Filter by tag..."
value={(*tag_filter).clone()}
oninput={on_tag_input}
/>
</div>
if *loading {
<p class="page-browse__loading">{ "Loading..." }</p>
} else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} />
} else if quotes.is_empty() {
<p class="page-browse__empty">{ "No quotes found." }</p>
} else {
<div class="page-browse__list">
{ for (*quotes).iter().map(|q| html! {
<QuoteCard quote={q.clone()} />
}) }
</div>
<Pagination
page={*page}
total_pages={*total_pages}
on_page={on_page}
/>
}
</div>
}
}