meal filtering feature
This commit is contained in:
+111
-49
@@ -293,6 +293,7 @@ pub fn meals_page(
|
||||
meals: &[Meal],
|
||||
meal_categories: &[MealCategory],
|
||||
csrf_token: &str,
|
||||
q: &str,
|
||||
) -> Markup {
|
||||
page(
|
||||
"Meals",
|
||||
@@ -306,7 +307,7 @@ pub fn meals_page(
|
||||
}
|
||||
a class="button button-primary" href="/meals/new" { "New meal" }
|
||||
}
|
||||
@if meals.is_empty() {
|
||||
@if meals.is_empty() && q.is_empty() {
|
||||
div class="empty-state" {
|
||||
div class="empty-mark" { "🍽" }
|
||||
h3 { "No meals yet" }
|
||||
@@ -319,28 +320,21 @@ pub fn meals_page(
|
||||
h2 { "All meals" }
|
||||
span class="count-badge" { (meals.len()) }
|
||||
}
|
||||
@if meals.is_empty() {
|
||||
p class="muted" { "Create a meal to get started." }
|
||||
} @else {
|
||||
@for (category_name, category_meals) in meal_groups(meals, meal_categories) {
|
||||
div class="category-group" {
|
||||
div class="category-heading" {
|
||||
h3 { (category_name) " (" (category_meals.len()) ")" }
|
||||
}
|
||||
div class="list-cards" {
|
||||
@for meal in category_meals {
|
||||
a class="list-card" href=(format!("/meals/{}", meal.id)) {
|
||||
span class="list-card-icon" { "🍽" }
|
||||
span class="list-card-copy" {
|
||||
strong { (meal.name) }
|
||||
small { (meal.ingredients.len()) " ingredients" }
|
||||
}
|
||||
span class="list-card-arrow" { "→" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="meal-filter-bar" {
|
||||
input
|
||||
class="meal-filter"
|
||||
id="meal-search"
|
||||
type="search"
|
||||
name="q"
|
||||
value=(q)
|
||||
placeholder="Search meals"
|
||||
hx-get="/meals"
|
||||
hx-trigger="input changed delay:200ms"
|
||||
hx-target="#meal-results"
|
||||
hx-swap="innerHTML";
|
||||
}
|
||||
div id="meal-results" {
|
||||
(meal_results(meals, meal_categories, q))
|
||||
}
|
||||
}
|
||||
aside class="side-column" {
|
||||
@@ -376,6 +370,42 @@ pub fn meals_page(
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders the meals page's grouped results, or the appropriate empty state.
|
||||
/// Distinct from the database-empty state: when a search yields no matches the
|
||||
/// list is empty (pre-filtered in SQL) even though meals exist, so we show a
|
||||
/// search-specific message.
|
||||
pub fn meal_results(meals: &[Meal], meal_categories: &[MealCategory], q: &str) -> Markup {
|
||||
html! {
|
||||
@if meals.is_empty() && q.is_empty() {
|
||||
p class="muted" { "Create a meal to get started." }
|
||||
} @else if meals.is_empty() {
|
||||
div class="meal-filter-empty" {
|
||||
p { "No meals match your search." }
|
||||
}
|
||||
} @else {
|
||||
@for (category_name, category_meals) in meal_groups(meals, meal_categories) {
|
||||
div class="category-group" {
|
||||
div class="category-heading" {
|
||||
h3 { (category_name) " (" (category_meals.len()) ")" }
|
||||
}
|
||||
div class="list-cards" {
|
||||
@for meal in category_meals {
|
||||
a class="list-card" href=(format!("/meals/{}", meal.id)) {
|
||||
span class="list-card-icon" { "🍽" }
|
||||
span class="list-card-copy" {
|
||||
strong { (meal.name) }
|
||||
small { (meal.ingredients.len()) " ingredients" }
|
||||
}
|
||||
span class="list-card-arrow" { "→" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn meal_groups<'a>(
|
||||
meals: &'a [Meal],
|
||||
meal_categories: &[MealCategory],
|
||||
@@ -416,6 +446,7 @@ pub fn meal_picker(
|
||||
meal_categories: &[MealCategory],
|
||||
list_id: i64,
|
||||
csrf_token: &str,
|
||||
q: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="meal-picker-backdrop" onclick="if (event.target === this) this.remove()" {
|
||||
@@ -427,7 +458,19 @@ pub fn meal_picker(
|
||||
}
|
||||
button class="meal-picker-close" type="button" aria-label="Close" onclick="this.closest('.meal-picker-backdrop').remove()" { "✕" }
|
||||
}
|
||||
@if meals.is_empty() {
|
||||
div class="meal-filter-bar meal-filter-bar-picker" {
|
||||
input
|
||||
class="meal-filter"
|
||||
name="q"
|
||||
type="search"
|
||||
value=(q)
|
||||
placeholder="Search meals"
|
||||
hx-get=(format!("/meals?picker={}", list_id))
|
||||
hx-trigger="input changed delay:200ms"
|
||||
hx-target="#meal-picker-results"
|
||||
hx-swap="innerHTML";
|
||||
}
|
||||
@if meals.is_empty() && q.is_empty() {
|
||||
div class="meal-picker-empty" {
|
||||
span class="empty-mark" { "🍽" }
|
||||
h3 { "No meals yet" }
|
||||
@@ -435,32 +478,51 @@ pub fn meal_picker(
|
||||
a class="button button-primary" href="/meals/new" { "Create a meal" }
|
||||
}
|
||||
} @else {
|
||||
div class="meal-picker-list" {
|
||||
@for (category_name, category_meals) in meal_groups(meals, meal_categories) {
|
||||
div class="category-group" {
|
||||
div class="category-heading" {
|
||||
h3 { (category_name) }
|
||||
}
|
||||
@for meal in category_meals {
|
||||
form
|
||||
hx-post=(format!("/lists/{}/add-meal", list_id))
|
||||
hx-target="#list-items"
|
||||
hx-swap="morph:outerHTML"
|
||||
hx-on::after-request="if (event.detail.successful) this.closest('.meal-picker-backdrop').remove()"
|
||||
class="meal-picker-row"
|
||||
{
|
||||
input type="hidden" name="csrf" value=(csrf_token);
|
||||
input type="hidden" name="meal_id" value=(meal.id);
|
||||
button class="meal-picker-button" type="submit" {
|
||||
span class="meal-picker-icon" { "🍽" }
|
||||
span class="meal-picker-copy" {
|
||||
strong { (meal.name) }
|
||||
small { (meal.ingredients.len()) " ingredients" }
|
||||
}
|
||||
span class="meal-picker-add" { "Add" }
|
||||
}
|
||||
}
|
||||
div id="meal-picker-results" class="meal-picker-list" {
|
||||
(meal_picker_results(meals, meal_categories, list_id, csrf_token))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The grouped picker rows, or the search-specific empty state. Kept separate
|
||||
/// from the database-empty state above.
|
||||
pub fn meal_picker_results(
|
||||
meals: &[Meal],
|
||||
meal_categories: &[MealCategory],
|
||||
list_id: i64,
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
@if meals.is_empty() {
|
||||
div class="meal-picker-empty" {
|
||||
p { "No meals match your search." }
|
||||
}
|
||||
} @else {
|
||||
@for (category_name, category_meals) in meal_groups(meals, meal_categories) {
|
||||
div class="category-group" {
|
||||
div class="category-heading" {
|
||||
h3 { (category_name) }
|
||||
}
|
||||
@for meal in category_meals {
|
||||
form
|
||||
hx-post=(format!("/lists/{}/add-meal", list_id))
|
||||
hx-target="#list-items"
|
||||
hx-swap="morph:outerHTML"
|
||||
hx-on::after-request="if (event.detail.successful) this.closest('.meal-picker-backdrop').remove()"
|
||||
class="meal-picker-row"
|
||||
{
|
||||
input type="hidden" name="csrf" value=(csrf_token);
|
||||
input type="hidden" name="meal_id" value=(meal.id);
|
||||
button class="meal-picker-button" type="submit" {
|
||||
span class="meal-picker-icon" { "🍽" }
|
||||
span class="meal-picker-copy" {
|
||||
strong { (meal.name) }
|
||||
small { (meal.ingredients.len()) " ingredients" }
|
||||
}
|
||||
span class="meal-picker-add" { "Add" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user