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
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>
|
|
}
|
|
}
|