feat(quotesdb): collapsible filter panel on browse page

Add a toggle button ("Filters ▼ / ▲") above the quote list on the
browse page. Filter controls are hidden by default and expand when
clicked. Each filter (Author, Tag, Date range) is on its own labelled
row with consistent styling. Existing API query logic is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Elijah Voigt 3 months ago
parent ecb1df1a2a
commit 2adbc95645

@ -14,6 +14,9 @@ use yew::prelude::*;
/// Displays a paginated list of quotes. Supports filtering by author name, /// Displays a paginated list of quotes. Supports filtering by author name,
/// tag, and an optional date range (year granularity). Fetches from the API /// tag, and an optional date range (year granularity). Fetches from the API
/// whenever page, author, tag, or date state changes. /// whenever page, author, tag, or date state changes.
///
/// Filter controls are hidden behind a collapsible "Filters" toggle button.
/// The panel is collapsed by default when the page loads.
#[function_component(BrowsePage)] #[function_component(BrowsePage)]
pub fn browse_page() -> Html { pub fn browse_page() -> Html {
let page = use_state(|| 1u32); let page = use_state(|| 1u32);
@ -25,6 +28,8 @@ pub fn browse_page() -> Html {
let quotes: UseStateHandle<Vec<Quote>> = use_state(Vec::new); let quotes: UseStateHandle<Vec<Quote>> = use_state(Vec::new);
let error: UseStateHandle<Option<String>> = use_state(|| None); let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true); let loading = use_state(|| true);
// Filter panel is collapsed by default.
let filters_open = use_state(|| false);
// Fetch quotes whenever page, author, tag, or date bounds change // Fetch quotes whenever page, author, tag, or date bounds change
{ {
@ -143,29 +148,63 @@ pub fn browse_page() -> Html {
}) })
}; };
let on_toggle_filters = {
let filters_open = filters_open.clone();
Callback::from(move |_: MouseEvent| {
filters_open.set(!*filters_open);
})
};
let toggle_label = if *filters_open {
"Filters \u{25b2}"
} else {
"Filters \u{25bc}"
};
html! { html! {
<div class="page-browse"> <div class="page-browse">
<h1 class="page-browse__title">{ "Browse Quotes" }</h1> <h1 class="page-browse__title">{ "Browse Quotes" }</h1>
<div class="page-browse__filter-toggle-row">
<button
class="btn page-browse__filter-toggle"
type="button"
onclick={on_toggle_filters}
>
{ toggle_label }
</button>
</div>
if *filters_open {
<div class="page-browse__filters"> <div class="page-browse__filters">
<div class="page-browse__filter-row">
<label class="page-browse__filter-label" for="browse-author">{ "Author:" }</label>
<input <input
id="browse-author"
class="page-browse__filter-input" class="page-browse__filter-input"
type="text" type="text"
placeholder="Filter by author..." placeholder="Filter by author..."
value={(*author_filter).clone()} value={(*author_filter).clone()}
oninput={on_author_input} oninput={on_author_input}
/> />
</div>
<div class="page-browse__filter-row">
<label class="page-browse__filter-label" for="browse-tag">{ "Tag:" }</label>
<input <input
id="browse-tag"
class="page-browse__filter-input" class="page-browse__filter-input"
type="text" type="text"
placeholder="Filter by tag..." placeholder="Filter by tag..."
value={(*tag_filter).clone()} value={(*tag_filter).clone()}
oninput={on_tag_input} oninput={on_tag_input}
/> />
<div class="browse-filter__group"> </div>
<label>{ "From year" }</label> <div class="page-browse__filter-row">
<label class="page-browse__filter-label">{ "Date:" }</label>
<div class="page-browse__filter-date-group">
<span class="page-browse__filter-date-label">{ "after" }</span>
<input <input
class="page-browse__filter-input" class="page-browse__filter-input page-browse__filter-input--year"
type="number" type="number"
min="0" min="0"
max="9999" max="9999"
@ -173,11 +212,9 @@ pub fn browse_page() -> Html {
value={(*date_after_year).clone()} value={(*date_after_year).clone()}
oninput={on_date_after_year_input} oninput={on_date_after_year_input}
/> />
</div> <span class="page-browse__filter-date-label">{ "before" }</span>
<div class="browse-filter__group">
<label>{ "To year" }</label>
<input <input
class="page-browse__filter-input" class="page-browse__filter-input page-browse__filter-input--year"
type="number" type="number"
min="0" min="0"
max="9999" max="9999"
@ -187,6 +224,8 @@ pub fn browse_page() -> Html {
/> />
</div> </div>
</div> </div>
</div>
}
if *loading { if *loading {
<p class="page-browse__loading">{ "Loading..." }</p> <p class="page-browse__loading">{ "Loading..." }</p>

@ -419,15 +419,69 @@ code {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
/* Row containing the toggle button, sits above the collapsible panel */
.page-browse__filter-toggle-row {
margin-bottom: 0.75rem;
}
/* Toggle button — inherits .btn base styles */
.page-browse__filter-toggle {
font-size: 0.9rem;
}
/* Collapsible filter panel — shown only when open */
.page-browse__filters { .page-browse__filters {
display: flex; display: flex;
gap: 1rem; flex-direction: column;
margin-bottom: 2rem; gap: 0.75rem;
flex-wrap: wrap; background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
} }
/* A single filter row: label + input(s) on one line */
.page-browse__filter-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Fixed-width label so all inputs line up vertically */
.page-browse__filter-label {
flex-shrink: 0;
width: 4.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text);
text-align: right;
}
/* Text / number inputs inside the filter panel */
.page-browse__filter-input { .page-browse__filter-input {
max-width: 240px; flex: 1;
max-width: 260px;
}
/* Narrower year inputs for the date range row */
.page-browse__filter-input--year {
max-width: 120px;
}
/* Inner group for the date range row: "after [input] before [input]" */
.page-browse__filter-date-group {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Small inline text labels ("after" / "before") */
.page-browse__filter-date-label {
font-size: 0.875rem;
color: var(--color-text-muted);
white-space: nowrap;
} }
.page-browse__loading, .page-browse__loading,
@ -611,15 +665,21 @@ code {
align-items: center; align-items: center;
} }
.page-browse__filters { .page-browse__filter-row {
flex-direction: column; flex-wrap: wrap;
} }
.page-browse__filter-input, .page-browse__filter-input,
.page-browse__filter-input--year,
.page-author__filter-input { .page-author__filter-input {
max-width: 100%; max-width: 100%;
} }
.page-browse__filter-date-group {
flex-direction: column;
align-items: flex-start;
}
.nav { .nav {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
} }

Loading…
Cancel
Save