From f6b51ccb389dd4dfccb5c7606905944598c7d8da Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 10 Feb 2026 09:37:49 +0000 Subject: [PATCH] refactor: reduce duplication in handlers, templates, and view structs - Replace inline redirect scripts with existing render_redirect_script() in 6 create handlers - Add delete_button macro to detail_cards.html, used by 6 detail pages - Unify 3 map_with_legend macros into 1 with LegendEntry iteration --- src/application/routes/api/coffee/cafes.rs | 13 +-- src/application/routes/api/coffee/checkin.rs | 15 +--- src/application/routes/api/coffee/cups.rs | 13 +-- src/application/routes/api/coffee/gear.rs | 13 +-- src/application/routes/api/coffee/roasters.rs | 13 +-- src/application/routes/api/coffee/roasts.rs | 13 +-- src/presentation/web/views/bags.rs | 13 ++- src/presentation/web/views/brews.rs | 13 ++- src/presentation/web/views/cafes.rs | 7 +- src/presentation/web/views/cups.rs | 17 +++- src/presentation/web/views/mod.rs | 6 ++ src/presentation/web/views/roasters.rs | 7 +- src/presentation/web/views/roasts.rs | 13 ++- templates/pages/bag.html | 2 +- templates/pages/brew.html | 12 +-- templates/pages/cafe.html | 12 +-- templates/pages/cup.html | 12 +-- templates/pages/gear.html | 11 +-- templates/pages/roast.html | 12 +-- templates/pages/roaster.html | 12 +-- templates/partials/detail_cards.html | 88 ++++--------------- 21 files changed, 115 insertions(+), 202 deletions(-) diff --git a/src/application/routes/api/coffee/cafes.rs b/src/application/routes/api/coffee/cafes.rs index b38ce38..4fde57c 100644 --- a/src/application/routes/api/coffee/cafes.rs +++ b/src/application/routes/api/coffee/cafes.rs @@ -10,7 +10,7 @@ use crate::application::routes::api::macros::{ define_delete_handler, define_get_handler, define_list_fragment_renderer, }; use crate::application::routes::support::{ - FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, + FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script, }; use crate::application::state::AppState; use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe}; @@ -89,16 +89,7 @@ pub(crate) async fn create_cafe( .await .map_err(ApiError::from) } else { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) diff --git a/src/application/routes/api/coffee/checkin.rs b/src/application/routes/api/coffee/checkin.rs index 702513c..2a8779e 100644 --- a/src/application/routes/api/coffee/checkin.rs +++ b/src/application/routes/api/coffee/checkin.rs @@ -6,7 +6,9 @@ use serde::Deserialize; use crate::application::auth::AuthenticatedUser; use crate::application::errors::{ApiError, AppError}; -use crate::application::routes::support::{FlexiblePayload, PayloadSource, is_datastar_request}; +use crate::application::routes::support::{ + FlexiblePayload, PayloadSource, is_datastar_request, render_redirect_script, +}; use crate::application::state::AppState; use crate::domain::cafes::NewCafe; use crate::domain::cups::NewCup; @@ -92,16 +94,7 @@ pub(crate) async fn submit_checkin( let detail_url = format!("/cups/{}", cup.id); if is_datastar_request(&headers) { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) } else { diff --git a/src/application/routes/api/coffee/cups.rs b/src/application/routes/api/coffee/cups.rs index 759edcf..5ad38a3 100644 --- a/src/application/routes/api/coffee/cups.rs +++ b/src/application/routes/api/coffee/cups.rs @@ -9,7 +9,7 @@ use crate::application::routes::api::macros::{ define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer, }; use crate::application::routes::support::{ - FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, + FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script, }; use crate::application::state::AppState; use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup}; @@ -67,16 +67,7 @@ pub(crate) async fn create_cup( let detail_url = format!("/cups/{}", cup.id); if is_datastar_request(&headers) { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) } else { diff --git a/src/application/routes/api/coffee/gear.rs b/src/application/routes/api/coffee/gear.rs index e0e4a3a..0ffebb4 100644 --- a/src/application/routes/api/coffee/gear.rs +++ b/src/application/routes/api/coffee/gear.rs @@ -14,7 +14,7 @@ use crate::application::routes::api::macros::{ define_delete_handler, define_get_handler, define_list_fragment_renderer, }; use crate::application::routes::support::{ - FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, + FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script, }; use crate::application::state::AppState; use crate::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear, UpdateGear}; @@ -82,16 +82,7 @@ pub(crate) async fn create_gear( .await .map_err(ApiError::from) } else { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) diff --git a/src/application/routes/api/coffee/roasters.rs b/src/application/routes/api/coffee/roasters.rs index 275fb89..a6ce5d9 100644 --- a/src/application/routes/api/coffee/roasters.rs +++ b/src/application/routes/api/coffee/roasters.rs @@ -9,7 +9,7 @@ use crate::application::routes::api::macros::{ define_delete_handler, define_get_handler, define_list_fragment_renderer, }; use crate::application::routes::support::{ - FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, + FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script, }; use crate::application::state::AppState; use crate::domain::ids::RoasterId; @@ -90,16 +90,7 @@ pub(crate) async fn create_roaster( .await .map_err(ApiError::from) } else { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) diff --git a/src/application/routes/api/coffee/roasts.rs b/src/application/routes/api/coffee/roasts.rs index 0156c62..0c66f47 100644 --- a/src/application/routes/api/coffee/roasts.rs +++ b/src/application/routes/api/coffee/roasts.rs @@ -11,7 +11,7 @@ use crate::application::routes::api::macros::{ define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer, }; use crate::application::routes::support::{ - FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, + FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, render_redirect_script, }; use crate::application::state::AppState; use crate::domain::ids::{RoastId, RoasterId}; @@ -92,16 +92,7 @@ pub(crate) async fn create_roast( .await .map_err(ApiError::from) } else { - use axum::http::header::HeaderValue; - let script = format!(""); - let mut response = axum::response::Html(script).into_response(); - response - .headers_mut() - .insert("datastar-selector", HeaderValue::from_static("body")); - response - .headers_mut() - .insert("datastar-mode", HeaderValue::from_static("append")); - Ok(response) + render_redirect_script(&detail_url).map_err(ApiError::from) } } else if matches!(source, PayloadSource::Form) { Ok(Redirect::to(&detail_url).into_response()) diff --git a/src/presentation/web/views/bags.rs b/src/presentation/web/views/bags.rs index 6d36d8e..43a634d 100644 --- a/src/presentation/web/views/bags.rs +++ b/src/presentation/web/views/bags.rs @@ -4,7 +4,7 @@ use crate::domain::roasters::Roaster; use crate::domain::roasts::Roast; use super::tasting_notes::TastingNoteView; -use super::{build_coffee_info, build_map_data, build_roaster_info}; +use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info}; #[derive(Debug, Clone)] pub struct BagView { @@ -89,6 +89,7 @@ pub struct BagDetailView { // Map pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, // Slugs (for breadcrumbs) pub roaster_slug: String, pub roast_slug: String, @@ -143,6 +144,16 @@ impl BagDetailView { .map(|d| d.format("%Y-%m-%d").to_string()), map_countries, map_max, + legend_entries: vec![ + LegendEntry { + label: "Origin", + opacity: "", + }, + LegendEntry { + label: "Roaster", + opacity: "opacity-50", + }, + ], created_date: bag.bag.created_at.format("%Y-%m-%d").to_string(), created_time: bag.bag.created_at.format("%H:%M").to_string(), } diff --git a/src/presentation/web/views/brews.rs b/src/presentation/web/views/brews.rs index f3c756e..53af38a 100644 --- a/src/presentation/web/views/brews.rs +++ b/src/presentation/web/views/brews.rs @@ -6,7 +6,7 @@ use crate::domain::roasters::Roaster; use crate::domain::roasts::Roast; use super::tasting_notes::TastingNoteView; -use super::{build_coffee_info, build_map_data, build_roaster_info, relative_date}; +use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info, relative_date}; #[derive(Clone)] pub struct QuickNoteView { @@ -222,6 +222,7 @@ pub struct BrewDetailView { // Map pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, // Slugs (for breadcrumbs) pub roaster_slug: String, pub roast_slug: String, @@ -279,6 +280,16 @@ impl BrewDetailView { filter_paper_name: brew.filter_paper_name, map_countries, map_max, + legend_entries: vec![ + LegendEntry { + label: "Origin", + opacity: "", + }, + LegendEntry { + label: "Roaster", + opacity: "opacity-50", + }, + ], created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(), created_time: brew.brew.created_at.format("%H:%M").to_string(), } diff --git a/src/presentation/web/views/cafes.rs b/src/presentation/web/views/cafes.rs index 05af75c..7d5261f 100644 --- a/src/presentation/web/views/cafes.rs +++ b/src/presentation/web/views/cafes.rs @@ -2,7 +2,7 @@ use crate::domain::cafes::Cafe; use crate::domain::countries::{country_to_iso, iso_to_flag_emoji}; use crate::infrastructure::foursquare::NearbyCafe; -use super::build_map_data; +use super::{LegendEntry, build_map_data}; pub struct CafeDetailView { pub id: String, @@ -13,6 +13,7 @@ pub struct CafeDetailView { pub website: Option, pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, pub created_date: String, pub created_time: String, } @@ -33,6 +34,10 @@ impl CafeDetailView { website: cafe.website, map_countries, map_max, + legend_entries: vec![LegendEntry { + label: "Cafe", + opacity: "", + }], created_date: cafe.created_at.format("%Y-%m-%d").to_string(), created_time: cafe.created_at.format("%H:%M").to_string(), } diff --git a/src/presentation/web/views/cups.rs b/src/presentation/web/views/cups.rs index e135d6a..73e4cad 100644 --- a/src/presentation/web/views/cups.rs +++ b/src/presentation/web/views/cups.rs @@ -5,7 +5,7 @@ use crate::domain::roasters::Roaster; use crate::domain::roasts::Roast; use super::tasting_notes::TastingNoteView; -use super::{build_coffee_info, build_map_data, build_roaster_info}; +use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info}; #[derive(Clone)] pub struct CupView { @@ -63,6 +63,7 @@ pub struct CupDetailView { // Map pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, // Slugs (for breadcrumbs) pub roaster_slug: String, pub roast_slug: String, @@ -115,6 +116,20 @@ impl CupDetailView { cafe_slug: cafe.slug.clone(), map_countries, map_max, + legend_entries: vec![ + LegendEntry { + label: "Cafe", + opacity: "", + }, + LegendEntry { + label: "Origin", + opacity: "opacity-65", + }, + LegendEntry { + label: "Roaster", + opacity: "opacity-35", + }, + ], created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(), created_time: cup.cup.created_at.format("%H:%M").to_string(), } diff --git a/src/presentation/web/views/mod.rs b/src/presentation/web/views/mod.rs index b5a6cf1..a2ab9f1 100644 --- a/src/presentation/web/views/mod.rs +++ b/src/presentation/web/views/mod.rs @@ -361,6 +361,12 @@ fn page_size_from_text(value: &str) -> PageSize { } } +/// A label + opacity pair for map legend entries on detail pages. +pub struct LegendEntry { + pub label: &'static str, + pub opacity: &'static str, +} + /// Shared coffee info fields extracted from a `Roast` for detail pages. pub(crate) struct CoffeeInfo { pub origin: String, diff --git a/src/presentation/web/views/roasters.rs b/src/presentation/web/views/roasters.rs index a7d58d0..2b9cc28 100644 --- a/src/presentation/web/views/roasters.rs +++ b/src/presentation/web/views/roasters.rs @@ -1,7 +1,7 @@ use crate::domain::countries::{country_to_iso, iso_to_flag_emoji}; use crate::domain::roasters::Roaster; -use super::build_map_data; +use super::{LegendEntry, build_map_data}; pub struct RoasterDetailView { pub id: String, @@ -12,6 +12,7 @@ pub struct RoasterDetailView { pub homepage: Option, pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, pub created_date: String, pub created_time: String, } @@ -32,6 +33,10 @@ impl RoasterDetailView { homepage: roaster.homepage, map_countries, map_max, + legend_entries: vec![LegendEntry { + label: "Roaster", + opacity: "", + }], created_date: roaster.created_at.format("%Y-%m-%d").to_string(), created_time: roaster.created_at.format("%H:%M").to_string(), } diff --git a/src/presentation/web/views/roasts.rs b/src/presentation/web/views/roasts.rs index 37ae5f0..8e7c18b 100644 --- a/src/presentation/web/views/roasts.rs +++ b/src/presentation/web/views/roasts.rs @@ -3,7 +3,7 @@ use crate::domain::roasters::Roaster; use crate::domain::roasts::{Roast, RoastWithRoaster}; use super::tasting_notes::{self, TastingNoteView}; -use super::{build_coffee_info, build_map_data, build_roaster_info}; +use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info}; pub struct RoastView { pub id: String, @@ -120,6 +120,7 @@ pub struct RoastDetailView { // Map pub map_countries: String, pub map_max: u32, + pub legend_entries: Vec, // Dates pub created_date: String, pub created_time: String, @@ -156,6 +157,16 @@ impl RoastDetailView { roaster_homepage: roaster_info.homepage, map_countries, map_max, + legend_entries: vec![ + LegendEntry { + label: "Origin", + opacity: "", + }, + LegendEntry { + label: "Roaster", + opacity: "opacity-50", + }, + ], created_date: roast.created_at.format("%Y-%m-%d").to_string(), created_time: roast.created_at.format("%H:%M").to_string(), } diff --git a/templates/pages/bag.html b/templates/pages/bag.html index 5790cd6..b19bc6f 100644 --- a/templates/pages/bag.html +++ b/templates/pages/bag.html @@ -32,7 +32,7 @@ {# ── Coffee + map ── #}
{{ detail::coffee_card(bag.roast_name, bag.roaster_name, bag.origin, bag.origin_flag, bag.region, bag.producer, bag.process, bag.tasting_notes, roaster_slug, roast_slug) }} - {{ detail::map_with_legend_2(bag.map_countries, bag.map_max, "Origin", "", "Roaster", "opacity-50") }} + {{ detail::map_with_legend(bag.map_countries, bag.map_max, bag.legend_entries) }}
{# ── Roaster & bag info ── #} diff --git a/templates/pages/brew.html b/templates/pages/brew.html index 881f15c..9444101 100644 --- a/templates/pages/brew.html +++ b/templates/pages/brew.html @@ -34,7 +34,7 @@ {# ── Coffee + map ── #}
{{ detail::coffee_card(brew.roast_name, brew.roaster_name, brew.origin, brew.origin_flag, brew.region, brew.producer, brew.process, brew.tasting_notes, roaster_slug, roast_slug) }} - {{ detail::map_with_legend_2(brew.map_countries, brew.map_max, "Origin", "", "Roaster", "opacity-50") }} + {{ detail::map_with_legend(brew.map_countries, brew.map_max, brew.legend_entries) }}
{# ── Roaster & gear ── #} @@ -100,14 +100,6 @@ {% if is_authenticated %} -
- -
+ {{ detail::delete_button("brew", "/api/v1/brews", brew.id) }} {% endif %} {% endblock %} diff --git a/templates/pages/cafe.html b/templates/pages/cafe.html index 560e94f..6c689b8 100644 --- a/templates/pages/cafe.html +++ b/templates/pages/cafe.html @@ -56,18 +56,10 @@ - {{ detail::map_with_legend_1(cafe.map_countries, cafe.map_max, "Cafe", "") }} + {{ detail::map_with_legend(cafe.map_countries, cafe.map_max, cafe.legend_entries) }} {% if is_authenticated %} -
- -
+ {{ detail::delete_button("cafe", "/api/v1/cafes", cafe.id) }} {% endif %} {% endblock %} diff --git a/templates/pages/cup.html b/templates/pages/cup.html index bb9b3e9..ded27de 100644 --- a/templates/pages/cup.html +++ b/templates/pages/cup.html @@ -32,7 +32,7 @@ {# ── Coffee + map ── #}
{{ detail::coffee_card(cup.roast_name, cup.roaster_name, cup.origin, cup.origin_flag, cup.region, cup.producer, cup.process, cup.tasting_notes, roaster_slug, roast_slug) }} - {{ detail::map_with_legend_3(cup.map_countries, cup.map_max, "Cafe", "", "Origin", "opacity-65", "Roaster", "opacity-35") }} + {{ detail::map_with_legend(cup.map_countries, cup.map_max, cup.legend_entries) }}
{# ── Roaster & cafe ── #} @@ -81,14 +81,6 @@ {% if is_authenticated %} -
- -
+ {{ detail::delete_button("cup", "/api/v1/cups", cup.id) }} {% endif %} {% endblock %} diff --git a/templates/pages/gear.html b/templates/pages/gear.html index 6daaab8..ed1976c 100644 --- a/templates/pages/gear.html +++ b/templates/pages/gear.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "partials/detail_cards.html" as detail %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog · {{ gear.make }} {{ gear.model }}{% endblock %} {% block og_title %}{{ gear.make }} {{ gear.model }} — Brewlog{% endblock %} @@ -44,14 +45,6 @@ {% if is_authenticated %} -
- -
+ {{ detail::delete_button("gear", "/api/v1/gear", gear.id) }} {% endif %} {% endblock %} diff --git a/templates/pages/roast.html b/templates/pages/roast.html index f79a5dc..63b9b3e 100644 --- a/templates/pages/roast.html +++ b/templates/pages/roast.html @@ -27,7 +27,7 @@
{{ detail::coffee_card(roast.name, roast.roaster_name, roast.origin, roast.origin_flag, roast.region, roast.producer, roast.process, roast.tasting_notes, roaster_slug, "") }} - {{ detail::map_with_legend_2(roast.map_countries, roast.map_max, "Origin", "", "Roaster", "opacity-50") }} + {{ detail::map_with_legend(roast.map_countries, roast.map_max, roast.legend_entries) }}
@@ -35,14 +35,6 @@
{% if is_authenticated %} -
- -
+ {{ detail::delete_button("roast", "/api/v1/roasts", roast.id) }} {% endif %} {% endblock %} diff --git a/templates/pages/roaster.html b/templates/pages/roaster.html index 54e7ee5..2cb5db0 100644 --- a/templates/pages/roaster.html +++ b/templates/pages/roaster.html @@ -63,18 +63,10 @@ - {{ detail::map_with_legend_1(roaster.map_countries, roaster.map_max, "Roaster", "") }} + {{ detail::map_with_legend(roaster.map_countries, roaster.map_max, roaster.legend_entries) }} {% if is_authenticated %} -
- -
+ {{ detail::delete_button("roaster", "/api/v1/roasters", roaster.id) }} {% endif %} {% endblock %} diff --git a/templates/partials/detail_cards.html b/templates/partials/detail_cards.html index 109442a..1fa801a 100644 --- a/templates/partials/detail_cards.html +++ b/templates/partials/detail_cards.html @@ -99,7 +99,7 @@ {% endmacro %} -{% macro map_with_legend_1(map_countries, map_max, label1, opacity1) %} +{% macro map_with_legend(map_countries, map_max, legends) %} {% if !map_countries.is_empty() %}
- - - {{ label1 }} - + {% for entry in legends %} + + + {{ entry.label }} + + {% endfor %}
{% endif %} {% endmacro %} -{% macro map_with_legend_2(map_countries, map_max, label1, opacity1, label2, opacity2) %} - {% if !map_countries.is_empty() %} -
+
- {% endif %} -{% endmacro %} - -{% macro map_with_legend_3(map_countries, map_max, label1, opacity1, label2, opacity2, label3, opacity3) %} - {% if !map_countries.is_empty() %} -
- -
- - - {{ label1 }} - - - - {{ label2 }} - - - - {{ label3 }} - -
-
- {% endif %} + {{ icons::delete("h-4 w-4") }} Delete + + {% endmacro %} {% macro roaster_card(name, country, country_flag, city, homepage, roaster_slug) %}