From 958c8de6cc7ba807ce79bf45b6afb7e97336b2ef Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Sun, 8 Feb 2026 17:20:46 +0000 Subject: [PATCH] feat(detail): add roaster, cafe, and gear detail pages - Add detail page routes, templates, and view models for roasters, cafes, and gear, following the existing pattern from bags/brews/cups - Add map_with_legend_1 macro for single-country detail maps - Redirect to detail page after entity creation instead of list page - Check referer in create handlers to return list fragment only from data page - Update bag delete button to use red background style - Update datastar tests to include referer header for create assertions --- src/application/routes/api/bags.rs | 30 ++++++++--- src/application/routes/api/cafes.rs | 30 ++++++++--- src/application/routes/api/gear.rs | 30 ++++++++--- src/application/routes/api/roasters.rs | 30 ++++++++--- src/application/routes/app/cafes.rs | 37 +++++++++++++ src/application/routes/app/gear.rs | 38 +++++++++++++ src/application/routes/app/mod.rs | 6 +++ src/application/routes/app/roasters.rs | 37 +++++++++++++ src/presentation/web/templates.rs | 37 +++++++++++-- src/presentation/web/views/cafes.rs | 38 +++++++++++++ src/presentation/web/views/gear.rs | 22 ++++++++ src/presentation/web/views/mod.rs | 6 +-- src/presentation/web/views/roasters.rs | 38 +++++++++++++ templates/pages/bag.html | 2 +- templates/pages/cafe.html | 72 +++++++++++++++++++++++++ templates/pages/gear.html | 62 +++++++++++++++++++++ templates/pages/roaster.html | 74 ++++++++++++++++++++++++++ templates/partials/detail_cards.html | 17 ++++++ tests/server/datastar.rs | 4 ++ 19 files changed, 579 insertions(+), 31 deletions(-) create mode 100644 src/application/routes/app/cafes.rs create mode 100644 src/application/routes/app/gear.rs create mode 100644 src/application/routes/app/roasters.rs create mode 100644 templates/pages/cafe.html create mode 100644 templates/pages/gear.html create mode 100644 templates/pages/roaster.html diff --git a/src/application/routes/api/bags.rs b/src/application/routes/api/bags.rs index 25b25ab..2eb67c8 100644 --- a/src/application/routes/api/bags.rs +++ b/src/application/routes/api/bags.rs @@ -72,14 +72,32 @@ pub(crate) async fn create_bag( info!(bag_id = %bag.id, "bag created"); state.stats_invalidator.invalidate(); + let detail_url = format!("/bags/{}", bag.id); + if is_datastar_request(&headers) { - render_bag_list_fragment(state, request, search, true) - .await - .map_err(ApiError::from) + let from_bag_page = headers + .get("referer") + .and_then(|v| v.to_str().ok()) + .is_some_and(|r| r.contains("type=bags")); + + if from_bag_page { + render_bag_list_fragment(state, request, search, true) + .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) + } } else if matches!(source, PayloadSource::Form) { - let target = - ListNavigator::new(BAG_PAGE_PATH, BAG_FRAGMENT_PATH, request, search).page_href(1); - Ok(Redirect::to(&target).into_response()) + Ok(Redirect::to(&detail_url).into_response()) } else { let enriched = state .bag_repo diff --git a/src/application/routes/api/cafes.rs b/src/application/routes/api/cafes.rs index c32ed1d..2f05427 100644 --- a/src/application/routes/api/cafes.rs +++ b/src/application/routes/api/cafes.rs @@ -74,14 +74,32 @@ pub(crate) async fn create_cafe( info!(cafe_id = %cafe.id, name = %cafe.name, "cafe created"); state.stats_invalidator.invalidate(); + let detail_url = format!("/cafes/{}", cafe.slug); + if is_datastar_request(&headers) { - render_cafe_list_fragment(state, request, search, true) - .await - .map_err(ApiError::from) + let from_data_page = headers + .get("referer") + .and_then(|v| v.to_str().ok()) + .is_some_and(|r| r.contains("type=cafes")); + + if from_data_page { + render_cafe_list_fragment(state, request, search, true) + .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) + } } else if matches!(source, PayloadSource::Form) { - let target = - ListNavigator::new(CAFE_PAGE_PATH, CAFE_FRAGMENT_PATH, request, search).page_href(1); - Ok(Redirect::to(&target).into_response()) + Ok(Redirect::to(&detail_url).into_response()) } else { Ok((StatusCode::CREATED, Json(cafe)).into_response()) } diff --git a/src/application/routes/api/gear.rs b/src/application/routes/api/gear.rs index 97d6069..eeeea6c 100644 --- a/src/application/routes/api/gear.rs +++ b/src/application/routes/api/gear.rs @@ -67,14 +67,32 @@ pub(crate) async fn create_gear( info!(gear_id = %gear.id, make = %gear.make, model = %gear.model, "gear created"); state.stats_invalidator.invalidate(); + let detail_url = format!("/gear/{}", gear.id); + if is_datastar_request(&headers) { - render_gear_list_fragment(state, request, search, true) - .await - .map_err(ApiError::from) + let from_data_page = headers + .get("referer") + .and_then(|v| v.to_str().ok()) + .is_some_and(|r| r.contains("type=gear")); + + if from_data_page { + render_gear_list_fragment(state, request, search, true) + .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) + } } else if matches!(source, PayloadSource::Form) { - let target = - ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search).page_href(1); - Ok(Redirect::to(&target).into_response()) + Ok(Redirect::to(&detail_url).into_response()) } else { Ok((StatusCode::CREATED, Json(gear)).into_response()) } diff --git a/src/application/routes/api/roasters.rs b/src/application/routes/api/roasters.rs index 0e402c0..03cc4a7 100644 --- a/src/application/routes/api/roasters.rs +++ b/src/application/routes/api/roasters.rs @@ -75,14 +75,32 @@ pub(crate) async fn create_roaster( info!(roaster_id = %roaster.id, name = %roaster.name, "roaster created"); state.stats_invalidator.invalidate(); + let detail_url = format!("/roasters/{}", roaster.slug); + if is_datastar_request(&headers) { - render_roaster_list_fragment(state, request, search, true) - .await - .map_err(ApiError::from) + let from_data_page = headers + .get("referer") + .and_then(|v| v.to_str().ok()) + .is_some_and(|r| r.contains("type=roasters")); + + if from_data_page { + render_roaster_list_fragment(state, request, search, true) + .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) + } } else if matches!(source, PayloadSource::Form) { - let target = ListNavigator::new(ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, request, search) - .page_href(1); - Ok(Redirect::to(&target).into_response()) + Ok(Redirect::to(&detail_url).into_response()) } else { Ok((StatusCode::CREATED, Json(roaster)).into_response()) } diff --git a/src/application/routes/app/cafes.rs b/src/application/routes/app/cafes.rs new file mode 100644 index 0000000..1ffd528 --- /dev/null +++ b/src/application/routes/app/cafes.rs @@ -0,0 +1,37 @@ +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use tower_cookies::Cookies; + +use crate::application::errors::map_app_error; +use crate::application::routes::render_html; +use crate::application::state::AppState; +use crate::presentation::web::templates::CafeDetailTemplate; +use crate::presentation::web::views::CafeDetailView; + +#[tracing::instrument(skip(state, cookies))] +pub(crate) async fn cafe_detail_page( + State(state): State, + cookies: Cookies, + Path(slug): Path, +) -> Result { + let is_authenticated = crate::application::routes::is_authenticated(&state, &cookies).await; + + let cafe = state + .cafe_repo + .get_by_slug(&slug) + .await + .map_err(|e| map_app_error(e.into()))?; + + let view = CafeDetailView::from_domain(cafe); + + let template = CafeDetailTemplate { + nav_active: "", + is_authenticated, + version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), + cafe: view, + }; + + render_html(template).map(IntoResponse::into_response) +} diff --git a/src/application/routes/app/gear.rs b/src/application/routes/app/gear.rs new file mode 100644 index 0000000..40f63d4 --- /dev/null +++ b/src/application/routes/app/gear.rs @@ -0,0 +1,38 @@ +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use tower_cookies::Cookies; + +use crate::application::errors::map_app_error; +use crate::application::routes::render_html; +use crate::application::state::AppState; +use crate::domain::ids::GearId; +use crate::presentation::web::templates::GearDetailTemplate; +use crate::presentation::web::views::GearDetailView; + +#[tracing::instrument(skip(state, cookies))] +pub(crate) async fn gear_detail_page( + State(state): State, + cookies: Cookies, + Path(id): Path, +) -> Result { + let is_authenticated = crate::application::routes::is_authenticated(&state, &cookies).await; + + let gear = state + .gear_repo + .get(id) + .await + .map_err(|e| map_app_error(e.into()))?; + + let view = GearDetailView::from_domain(gear); + + let template = GearDetailTemplate { + nav_active: "", + is_authenticated, + version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), + gear: view, + }; + + render_html(template).map(IntoResponse::into_response) +} diff --git a/src/application/routes/app/mod.rs b/src/application/routes/app/mod.rs index 304145d..be196fc 100644 --- a/src/application/routes/app/mod.rs +++ b/src/application/routes/app/mod.rs @@ -3,10 +3,13 @@ mod admin; pub(super) mod auth; mod bags; mod brews; +mod cafes; mod checkin; mod cups; mod data; +mod gear; mod home; +mod roasters; mod stats; mod timeline; mod webauthn; @@ -32,7 +35,10 @@ pub(super) fn router() -> axum::Router { .route("/stats", get(stats::stats_page)) .route("/bags/{id}", get(bags::bag_detail_page)) .route("/brews/{id}", get(brews::brew_detail_page)) + .route("/cafes/{slug}", get(cafes::cafe_detail_page)) .route("/cups/{id}", get(cups::cup_detail_page)) + .route("/gear/{id}", get(gear::gear_detail_page)) + .route("/roasters/{slug}", get(roasters::roaster_detail_page)) .route("/styles.css", get(styles)) .route("/webauthn.js", get(webauthn_js)) .route("/components/photo-capture.js", get(photo_capture_js)) diff --git a/src/application/routes/app/roasters.rs b/src/application/routes/app/roasters.rs new file mode 100644 index 0000000..1ee027a --- /dev/null +++ b/src/application/routes/app/roasters.rs @@ -0,0 +1,37 @@ +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use tower_cookies::Cookies; + +use crate::application::errors::map_app_error; +use crate::application::routes::render_html; +use crate::application::state::AppState; +use crate::presentation::web::templates::RoasterDetailTemplate; +use crate::presentation::web::views::RoasterDetailView; + +#[tracing::instrument(skip(state, cookies))] +pub(crate) async fn roaster_detail_page( + State(state): State, + cookies: Cookies, + Path(slug): Path, +) -> Result { + let is_authenticated = crate::application::routes::is_authenticated(&state, &cookies).await; + + let roaster = state + .roaster_repo + .get_by_slug(&slug) + .await + .map_err(|e| map_app_error(e.into()))?; + + let view = RoasterDetailView::from_domain(roaster); + + let template = RoasterDetailTemplate { + nav_active: "", + is_authenticated, + version_info: &crate::VERSION_INFO, + base_url: crate::base_url(), + roaster: view, + }; + + render_html(template).map(IntoResponse::into_response) +} diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index a28f8d1..b400d50 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -2,9 +2,10 @@ use askama::Template; use super::views::{ BagDetailView, BagOptionView, BagView, BrewDefaultsView, BrewDetailView, BrewView, - CafeOptionView, CafeView, CupDetailView, CupView, GearOptionView, GearView, ListNavigator, - NearbyCafeView, Paginated, QuickNoteView, RoastOptionView, RoastView, RoasterOptionView, - RoasterView, StatCard, StatsView, TimelineEventView, TimelineMonthView, + CafeDetailView, CafeOptionView, CafeView, CupDetailView, CupView, GearDetailView, + GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView, + RoastOptionView, RoastView, RoasterDetailView, RoasterOptionView, RoasterView, StatCard, + StatsView, TimelineEventView, TimelineMonthView, }; use crate::domain::bags::BagSortKey; use crate::domain::brews::BrewSortKey; @@ -240,6 +241,36 @@ pub struct CupDetailTemplate { pub cup: CupDetailView, } +#[derive(Template)] +#[template(path = "pages/roaster.html")] +pub struct RoasterDetailTemplate { + pub nav_active: &'static str, + pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, + pub roaster: RoasterDetailView, +} + +#[derive(Template)] +#[template(path = "pages/cafe.html")] +pub struct CafeDetailTemplate { + pub nav_active: &'static str, + pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, + pub cafe: CafeDetailView, +} + +#[derive(Template)] +#[template(path = "pages/gear.html")] +pub struct GearDetailTemplate { + pub nav_active: &'static str, + pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, + pub base_url: &'static str, + pub gear: GearDetailView, +} + pub fn render_template(template: T) -> Result { template.render() } diff --git a/src/presentation/web/views/cafes.rs b/src/presentation/web/views/cafes.rs index cf7c40b..71928f5 100644 --- a/src/presentation/web/views/cafes.rs +++ b/src/presentation/web/views/cafes.rs @@ -1,6 +1,44 @@ 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; + +pub struct CafeDetailView { + pub id: String, + pub name: String, + pub city: String, + pub country: String, + pub country_flag: String, + pub website: Option, + pub map_countries: String, + pub map_max: u32, + pub created_date: String, + pub created_time: String, +} + +impl CafeDetailView { + pub fn from_domain(cafe: Cafe) -> Self { + let country_flag = country_to_iso(&cafe.country) + .map(iso_to_flag_emoji) + .unwrap_or_default(); + let (map_countries, map_max) = build_map_data(&[(&cafe.country, 1)]); + + Self { + id: cafe.id.to_string(), + name: cafe.name, + city: cafe.city, + country_flag, + country: cafe.country, + website: cafe.website, + map_countries, + map_max, + created_date: cafe.created_at.format("%Y-%m-%d").to_string(), + created_time: cafe.created_at.format("%H:%M").to_string(), + } + } +} + pub struct CafeView { pub id: String, pub detail_path: String, diff --git a/src/presentation/web/views/gear.rs b/src/presentation/web/views/gear.rs index 5e103f2..c820ada 100644 --- a/src/presentation/web/views/gear.rs +++ b/src/presentation/web/views/gear.rs @@ -1,5 +1,27 @@ use crate::domain::gear::Gear; +pub struct GearDetailView { + pub id: String, + pub category_label: String, + pub make: String, + pub model: String, + pub created_date: String, + pub created_time: String, +} + +impl GearDetailView { + pub fn from_domain(gear: Gear) -> Self { + Self { + id: gear.id.to_string(), + category_label: gear.category.display_label().to_string(), + make: gear.make, + model: gear.model, + created_date: gear.created_at.format("%Y-%m-%d").to_string(), + created_time: gear.created_at.format("%H:%M").to_string(), + } + } +} + #[derive(Clone)] pub struct GearView { pub id: String, diff --git a/src/presentation/web/views/mod.rs b/src/presentation/web/views/mod.rs index 4624cc4..ae76c50 100644 --- a/src/presentation/web/views/mod.rs +++ b/src/presentation/web/views/mod.rs @@ -10,10 +10,10 @@ mod timeline; pub use bags::{BagDetailView, BagOptionView, BagView}; pub use brews::{BrewDefaultsView, BrewDetailView, BrewView, QuickNoteView}; -pub use cafes::{CafeOptionView, CafeView, NearbyCafeView}; +pub use cafes::{CafeDetailView, CafeOptionView, CafeView, NearbyCafeView}; pub use cups::{CupDetailView, CupView}; -pub use gear::{GearOptionView, GearView}; -pub use roasters::{RoasterOptionView, RoasterView}; +pub use gear::{GearDetailView, GearOptionView, GearView}; +pub use roasters::{RoasterDetailView, RoasterOptionView, RoasterView}; pub use roasts::{RoastOptionView, RoastView}; pub use tasting_notes::TastingNoteView; pub use timeline::{ diff --git a/src/presentation/web/views/roasters.rs b/src/presentation/web/views/roasters.rs index 0f6d36f..e1e713c 100644 --- a/src/presentation/web/views/roasters.rs +++ b/src/presentation/web/views/roasters.rs @@ -1,5 +1,43 @@ +use crate::domain::countries::{country_to_iso, iso_to_flag_emoji}; use crate::domain::roasters::Roaster; +use super::build_map_data; + +pub struct RoasterDetailView { + pub id: String, + pub name: String, + pub country: String, + pub country_flag: String, + pub city: Option, + pub homepage: Option, + pub map_countries: String, + pub map_max: u32, + pub created_date: String, + pub created_time: String, +} + +impl RoasterDetailView { + pub fn from_domain(roaster: Roaster) -> Self { + let country_flag = country_to_iso(&roaster.country) + .map(iso_to_flag_emoji) + .unwrap_or_default(); + let (map_countries, map_max) = build_map_data(&[(&roaster.country, 1)]); + + Self { + id: roaster.id.to_string(), + name: roaster.name, + country_flag, + country: roaster.country, + city: roaster.city, + homepage: roaster.homepage, + map_countries, + map_max, + created_date: roaster.created_at.format("%Y-%m-%d").to_string(), + created_time: roaster.created_at.format("%H:%M").to_string(), + } + } +} + pub struct RoasterOptionView { pub id: String, pub name: String, diff --git a/templates/pages/bag.html b/templates/pages/bag.html index ef95dfb..4d63275 100644 --- a/templates/pages/bag.html +++ b/templates/pages/bag.html @@ -82,7 +82,7 @@ {% endif %} diff --git a/templates/pages/cafe.html b/templates/pages/cafe.html new file mode 100644 index 0000000..c802dab --- /dev/null +++ b/templates/pages/cafe.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% import "partials/detail_cards.html" as detail %} +{% import "partials/icons.html" as icons %} +{% block title %}Brewlog · {{ cafe.name }}{% endblock %} +{% block og_title %}{{ cafe.name }} — Brewlog{% endblock %} +{% block og_description %}{{ cafe.name }} — {{ cafe.city }}, {{ cafe.country_flag }} {{ cafe.country }}{% endblock %} +{% block head %}{% endblock %} +{% block content %} +
+
+

{{ cafe.name }}

+

+ {{ cafe.city }}, {{ cafe.country_flag }} {{ cafe.country }} · Added {{ cafe.created_date }} +

+
+ {{ detail::share_button() }} +
+ +{{ detail::share_script() }} + +
+
+

Details

+
+
+
City
+
{{ cafe.city }}
+
+
+
Country
+
+ {% if !cafe.country_flag.is_empty() %}{{ cafe.country_flag }} {% endif %}{{ cafe.country }} +
+
+ {% if let Some(url) = cafe.website %} +
+
Website
+
+ Visit Website +
+
+ {% endif %} +
+
+ + {{ detail::map_with_legend_1(cafe.map_countries, cafe.map_max, "Cafe", "") }} +
+ +{% if is_authenticated %} +
+ +
+ +{% endif %} +{% endblock %} diff --git a/templates/pages/gear.html b/templates/pages/gear.html new file mode 100644 index 0000000..c290587 --- /dev/null +++ b/templates/pages/gear.html @@ -0,0 +1,62 @@ +{% 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 %} +{% block og_description %}{{ gear.make }} {{ gear.model }} — {{ gear.category_label }}{% endblock %} +{% block head %}{% endblock %} +{% block content %} +
+
+

{{ gear.make }} {{ gear.model }}

+

+ {{ gear.category_label }} · Added {{ gear.created_date }} +

+
+ {{ detail::share_button() }} +
+ +{{ detail::share_script() }} + +
+

Details

+
+
+
Category
+
{{ gear.category_label }}
+
+
+
Make
+
{{ gear.make }}
+
+
+
Model
+
{{ gear.model }}
+
+
+
+ +{% if is_authenticated %} +
+ +
+ +{% endif %} +{% endblock %} diff --git a/templates/pages/roaster.html b/templates/pages/roaster.html new file mode 100644 index 0000000..6cb6de0 --- /dev/null +++ b/templates/pages/roaster.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% import "partials/detail_cards.html" as detail %} +{% import "partials/icons.html" as icons %} +{% block title %}Brewlog · {{ roaster.name }}{% endblock %} +{% block og_title %}{{ roaster.name }} — Brewlog{% endblock %} +{% block og_description %}{{ roaster.name }} — {{ roaster.country_flag }} {{ roaster.country }}{% if let Some(c) = roaster.city %}, {{ c }}{% endif %}{% endblock %} +{% block head %}{% endblock %} +{% block content %} +
+
+

{{ roaster.name }}

+

+ {{ roaster.country_flag }} {{ roaster.country }}{% if let Some(c) = roaster.city %} · {{ c }}{% endif %} · Added {{ roaster.created_date }} +

+
+ {{ detail::share_button() }} +
+ +{{ detail::share_script() }} + +
+
+

Details

+
+
+
Country
+
+ {% if !roaster.country_flag.is_empty() %}{{ roaster.country_flag }} {% endif %}{{ roaster.country }} +
+
+ {% if let Some(c) = roaster.city %} +
+
City
+
{{ c }}
+
+ {% endif %} + {% if let Some(url) = roaster.homepage %} +
+
Website
+
+ Visit Website +
+
+ {% endif %} +
+
+ + {{ detail::map_with_legend_1(roaster.map_countries, roaster.map_max, "Roaster", "") }} +
+ +{% if is_authenticated %} +
+ +
+ +{% endif %} +{% endblock %} diff --git a/templates/partials/detail_cards.html b/templates/partials/detail_cards.html index 319546e..b848136 100644 --- a/templates/partials/detail_cards.html +++ b/templates/partials/detail_cards.html @@ -74,6 +74,23 @@ const shareLink = (btn) => { {% endmacro %} +{% macro map_with_legend_1(map_countries, map_max, label1, opacity1) %} +{% if !map_countries.is_empty() %} +
+ +
+ + {{ label1 }} + +
+
+{% endif %} +{% endmacro %} + {% macro map_with_legend_2(map_countries, map_max, label1, opacity1, label2, opacity2) %} {% if !map_countries.is_empty() %}
diff --git a/tests/server/datastar.rs b/tests/server/datastar.rs index 3af3ad9..0269556 100644 --- a/tests/server/datastar.rs +++ b/tests/server/datastar.rs @@ -115,6 +115,7 @@ async fn roasters_create_with_datastar_header_returns_fragment() { .post(app.api_url("/roasters")) .bearer_auth(app.auth_token.as_ref().unwrap()) .header("datastar-request", "true") + .header("referer", format!("{}/data?type=roasters", app.address)) .json(&new_roaster) .send() .await @@ -281,6 +282,7 @@ async fn bags_create_with_datastar_header_returns_fragment() { .post(app.api_url("/bags")) .bearer_auth(app.auth_token.as_ref().unwrap()) .header("datastar-request", "true") + .header("referer", format!("{}/data?type=bags", app.address)) .json(&new_bag) .send() .await @@ -415,6 +417,7 @@ async fn gear_create_with_datastar_header_returns_fragment() { .post(app.api_url("/gear")) .bearer_auth(app.auth_token.as_ref().unwrap()) .header("datastar-request", "true") + .header("referer", format!("{}/data?type=gear", app.address)) .json(&new_gear) .send() .await @@ -566,6 +569,7 @@ async fn cafes_create_with_datastar_header_returns_fragment() { .post(app.api_url("/cafes")) .bearer_auth(app.auth_token.as_ref().unwrap()) .header("datastar-request", "true") + .header("referer", format!("{}/data?type=cafes", app.address)) .json(&new_cafe) .send() .await