From 4ee8cf796468626af25cba520d48b260d899b518 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 10 Feb 2026 19:25:33 +0000 Subject: [PATCH] fix: polish edit forms and detail pages - Skip payload in tracing::instrument to avoid logging base64 image data - Add blob: to CSP img-src for image preview support - Add deferred_upload_with_preview macro for edit form image previews with Replace/Remove buttons and proper DOM cleanup on replacement - Fix datastar-fetch finished handler (evt.detail.response is undefined for redirect scripts) - Display brew time in M:SS format on edit form - Add full-width Save Changes button with check icon and Cancel button to all edit forms - Fix country flag emoji spacing on cafe and cup detail pages - Add "View on Map" Google Maps link to cafe and cup detail pages --- src/application/routes/api/coffee/bags.rs | 4 +- src/application/routes/api/coffee/brews.rs | 4 +- src/application/routes/api/coffee/cafes.rs | 4 +- src/application/routes/api/coffee/cups.rs | 4 +- src/application/routes/api/coffee/gear.rs | 4 +- src/application/routes/api/coffee/roasters.rs | 4 +- src/application/routes/api/coffee/roasts.rs | 4 +- src/application/routes/mod.rs | 2 +- src/presentation/web/views/cafes.rs | 6 +++ src/presentation/web/views/cups.rs | 5 ++ static/js/components/image-upload.js | 23 +++++++- templates/pages/cafe.html | 16 +++++- templates/pages/cup.html | 16 +++++- templates/pages/edit_bag.html | 14 +++-- templates/pages/edit_brew.html | 33 ++++++++---- templates/pages/edit_cafe.html | 16 ++++-- templates/pages/edit_cup.html | 16 ++++-- templates/pages/edit_gear.html | 16 ++++-- templates/pages/edit_roast.html | 16 ++++-- templates/pages/edit_roaster.html | 16 ++++-- templates/partials/image_section.html | 54 +++++++++++++++++++ 21 files changed, 225 insertions(+), 52 deletions(-) diff --git a/src/application/routes/api/coffee/bags.rs b/src/application/routes/api/coffee/bags.rs index e58aed4..2d10d80 100644 --- a/src/application/routes/api/coffee/bags.rs +++ b/src/application/routes/api/coffee/bags.rs @@ -52,7 +52,7 @@ pub(crate) async fn load_bag_page( Ok(BagPageData { bags, navigator }) } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_bag( State(state): State, _auth_user: AuthenticatedUser, @@ -156,7 +156,7 @@ impl UpdateBagSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn update_bag( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/brews.rs b/src/application/routes/api/coffee/brews.rs index 880ee43..ac14be3 100644 --- a/src/application/routes/api/coffee/brews.rs +++ b/src/application/routes/api/coffee/brews.rs @@ -238,7 +238,7 @@ impl NewBrewSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_brew( State(state): State, _auth_user: AuthenticatedUser, @@ -375,7 +375,7 @@ impl UpdateBrewSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_brew( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/cafes.rs b/src/application/routes/api/coffee/cafes.rs index ec47e17..23cb391 100644 --- a/src/application/routes/api/coffee/cafes.rs +++ b/src/application/routes/api/coffee/cafes.rs @@ -87,7 +87,7 @@ impl NewCafeSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_cafe( State(state): State, _auth_user: AuthenticatedUser, @@ -175,7 +175,7 @@ impl UpdateCafeSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_cafe( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/cups.rs b/src/application/routes/api/coffee/cups.rs index a170bfd..ca67edd 100644 --- a/src/application/routes/api/coffee/cups.rs +++ b/src/application/routes/api/coffee/cups.rs @@ -47,7 +47,7 @@ pub(crate) async fn load_cup_page( )) } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_cup( State(state): State, _auth_user: AuthenticatedUser, @@ -116,7 +116,7 @@ impl UpdateCupSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_cup( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/gear.rs b/src/application/routes/api/coffee/gear.rs index 81e92cc..2a4a3ef 100644 --- a/src/application/routes/api/coffee/gear.rs +++ b/src/application/routes/api/coffee/gear.rs @@ -49,7 +49,7 @@ pub(crate) async fn load_gear_page( )) } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_gear( State(state): State, _auth_user: AuthenticatedUser, @@ -148,7 +148,7 @@ impl UpdateGearSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_gear( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/roasters.rs b/src/application/routes/api/coffee/roasters.rs index a3e1a2d..5715094 100644 --- a/src/application/routes/api/coffee/roasters.rs +++ b/src/application/routes/api/coffee/roasters.rs @@ -86,7 +86,7 @@ impl NewRoasterSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_roaster( State(state): State, _auth_user: AuthenticatedUser, @@ -168,7 +168,7 @@ impl UpdateRoasterSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_roaster( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/api/coffee/roasts.rs b/src/application/routes/api/coffee/roasts.rs index 3acca15..7814209 100644 --- a/src/application/routes/api/coffee/roasts.rs +++ b/src/application/routes/api/coffee/roasts.rs @@ -48,7 +48,7 @@ pub(crate) async fn load_roast_page( )) } -#[tracing::instrument(skip(state, _auth_user, headers, query))] +#[tracing::instrument(skip(state, _auth_user, headers, query, payload))] pub(crate) async fn create_roast( State(state): State, _auth_user: AuthenticatedUser, @@ -214,7 +214,7 @@ impl UpdateRoastSubmission { } } -#[tracing::instrument(skip(state, _auth_user, headers))] +#[tracing::instrument(skip(state, _auth_user, headers, payload))] pub(crate) async fn update_roast( State(state): State, _auth_user: AuthenticatedUser, diff --git a/src/application/routes/mod.rs b/src/application/routes/mod.rs index 2f487c1..589783f 100644 --- a/src/application/routes/mod.rs +++ b/src/application/routes/mod.rs @@ -56,7 +56,7 @@ pub fn app_router(state: AppState) -> axum::Router { script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; \ style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \ font-src 'self' https://fonts.gstatic.com; \ - img-src 'self' data:; \ + img-src 'self' data: blob:; \ frame-ancestors 'none'", ), )) diff --git a/src/presentation/web/views/cafes.rs b/src/presentation/web/views/cafes.rs index 7d5261f..4cbf91c 100644 --- a/src/presentation/web/views/cafes.rs +++ b/src/presentation/web/views/cafes.rs @@ -11,6 +11,7 @@ pub struct CafeDetailView { pub country: String, pub country_flag: String, pub website: Option, + pub map_url: String, pub map_countries: String, pub map_max: u32, pub legend_entries: Vec, @@ -24,6 +25,10 @@ impl CafeDetailView { .map(iso_to_flag_emoji) .unwrap_or_default(); let (map_countries, map_max) = build_map_data(&[(&cafe.country, 1)]); + let map_url = format!( + "https://www.google.com/maps?q={},{}", + cafe.latitude, cafe.longitude + ); Self { id: cafe.id.to_string(), @@ -32,6 +37,7 @@ impl CafeDetailView { country_flag, country: cafe.country, website: cafe.website, + map_url, map_countries, map_max, legend_entries: vec![LegendEntry { diff --git a/src/presentation/web/views/cups.rs b/src/presentation/web/views/cups.rs index 73e4cad..da75839 100644 --- a/src/presentation/web/views/cups.rs +++ b/src/presentation/web/views/cups.rs @@ -60,6 +60,7 @@ pub struct CupDetailView { pub cafe_country: String, pub cafe_country_flag: String, pub cafe_website: Option, + pub cafe_map_url: String, // Map pub map_countries: String, pub map_max: u32, @@ -111,6 +112,10 @@ impl CupDetailView { cafe_country: cafe.country.clone(), cafe_country_flag, cafe_website: cafe.website.clone(), + cafe_map_url: format!( + "https://www.google.com/maps?q={},{}", + cafe.latitude, cafe.longitude + ), roaster_slug: roaster.slug.clone(), roast_slug: roast.slug.clone(), cafe_slug: cafe.slug.clone(), diff --git a/static/js/components/image-upload.js b/static/js/components/image-upload.js index a76c733..0681074 100644 --- a/static/js/components/image-upload.js +++ b/static/js/components/image-upload.js @@ -50,7 +50,28 @@ customElements.define( // Deferred mode: store data URL in a target hidden input const targetId = this.getAttribute("target-input"); if (targetId) { - document.getElementById(targetId).value = dataUrl; + const hiddenInput = document.getElementById(targetId); + hiddenInput.value = dataUrl; + + // Remove existing server image preview (edit forms) + const existing = document.getElementById(`${targetId}-existing`); + if (existing) { + existing.remove(); + // Create standalone preview after the hidden input since + // the Replace button (this element) was inside the removed container + let preview = hiddenInput.nextElementSibling; + if ( + !preview || + !preview.classList.contains("image-upload-preview") + ) { + preview = document.createElement("div"); + preview.className = + "image-upload-preview h-48 w-full bg-cover bg-center rounded-lg border"; + hiddenInput.insertAdjacentElement("afterend", preview); + } + preview.style.backgroundImage = `url('${dataUrl}')`; + return; + } } this._showPreview(dataUrl); } diff --git a/templates/pages/cafe.html b/templates/pages/cafe.html index 81c262d..9e09020 100644 --- a/templates/pages/cafe.html +++ b/templates/pages/cafe.html @@ -41,7 +41,21 @@
Country
- {% if !cafe.country_flag.is_empty() %}{{ cafe.country_flag }}{% endif %}{{ cafe.country }} + {% if !cafe.country_flag.is_empty() %} + {{ cafe.country_flag }} + {% endif %}{{ cafe.country }} +
+
+
+
Location
+
+ View on Map
{% if let Some(url) = cafe.website %} diff --git a/templates/pages/cup.html b/templates/pages/cup.html index 3c1cf0e..ede36ee 100644 --- a/templates/pages/cup.html +++ b/templates/pages/cup.html @@ -62,13 +62,27 @@
Country
- {% if !cup.cafe_country_flag.is_empty() %}{{ cup.cafe_country_flag }}{% endif %}{{ cup.cafe_country }} + {% if !cup.cafe_country_flag.is_empty() %} + {{ cup.cafe_country_flag }} + {% endif %}{{ cup.cafe_country }}
City
{{ cup.cafe_city }}
+
+
Location
+
+ View on Map +
+
{% if let Some(url) = cup.cafe_website %}
Website
diff --git a/templates/pages/edit_bag.html b/templates/pages/edit_bag.html index d21a39e..2fcf473 100644 --- a/templates/pages/edit_bag.html +++ b/templates/pages/edit_bag.html @@ -17,7 +17,7 @@ data-signals:_amount="{{ amount }}" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/bags/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated'); window.location.href = evt.detail.response.headers.get('location') || '/bags/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Bag updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -82,17 +82,25 @@ class="text-sm text-error" role="alert" >

-
+
+
diff --git a/templates/pages/edit_brew.html b/templates/pages/edit_brew.html index d2dbed8..4479dd0 100644 --- a/templates/pages/edit_brew.html +++ b/templates/pages/edit_brew.html @@ -27,7 +27,7 @@ data-signals:_qn-over-extracted="{% if quick_notes.contains("over-extracted") %}true{% else %}false{% endif %}" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/brews/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated'); window.location.href = evt.detail.response.headers.get('location') || '/brews/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Brew updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" > @@ -273,28 +273,31 @@
Time (s)Time
+ @@ -357,7 +360,7 @@ />
- {{ img::deferred_upload("edit-brew-image", "Brew Image") }} + {{ img::deferred_upload_with_preview("edit-brew-image", "Brew Image", "brew", id, image_url) }} -
+
+
diff --git a/templates/pages/edit_cafe.html b/templates/pages/edit_cafe.html index 6e43c7a..07ae990 100644 --- a/templates/pages/edit_cafe.html +++ b/templates/pages/edit_cafe.html @@ -22,7 +22,7 @@ data-signals:_website="'{{ website }}'" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cafes/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated'); window.location.href = evt.detail.response.headers.get('location') || '/cafes/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cafe updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -117,7 +117,7 @@ />
- {{ img::deferred_upload("edit-cafe-image", "Cafe Image") }} + {{ img::deferred_upload_with_preview("edit-cafe-image", "Cafe Image", "cafe", id, image_url) }} -
+
+
diff --git a/templates/pages/edit_cup.html b/templates/pages/edit_cup.html index e92c829..c98d433 100644 --- a/templates/pages/edit_cup.html +++ b/templates/pages/edit_cup.html @@ -16,7 +16,7 @@ data-signals:_submit-error="''" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/cups/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated'); window.location.href = evt.detail.response.headers.get('location') || '/cups/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Cup updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -71,7 +71,7 @@
- {{ img::deferred_upload("edit-cup-image", "Cup Image") }} + {{ img::deferred_upload_with_preview("edit-cup-image", "Cup Image", "cup", id, image_url) }} -
+
+
diff --git a/templates/pages/edit_gear.html b/templates/pages/edit_gear.html index 6db4047..e54e8aa 100644 --- a/templates/pages/edit_gear.html +++ b/templates/pages/edit_gear.html @@ -18,7 +18,7 @@ data-signals:_model="'{{ model }}'" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/gear/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated'); window.location.href = evt.detail.response.headers.get('location') || '/gear/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Gear updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -63,7 +63,7 @@ />
- {{ img::deferred_upload("edit-gear-image", "Gear Image") }} + {{ img::deferred_upload_with_preview("edit-gear-image", "Gear Image", "gear", id, image_url) }} -
+
+
diff --git a/templates/pages/edit_roast.html b/templates/pages/edit_roast.html index 2234f34..dddd09f 100644 --- a/templates/pages/edit_roast.html +++ b/templates/pages/edit_roast.html @@ -22,7 +22,7 @@ data-signals:_tasting-notes="'{{ tasting_notes }}'" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasts/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasts/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roast updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -139,7 +139,7 @@ >
- {{ img::deferred_upload("edit-roast-image", "Roast Image") }} + {{ img::deferred_upload_with_preview("edit-roast-image", "Roast Image", "roast", id, image_url) }} -
+
+
diff --git a/templates/pages/edit_roaster.html b/templates/pages/edit_roaster.html index 6ca7b3f..9a2d5ac 100644 --- a/templates/pages/edit_roaster.html +++ b/templates/pages/edit_roaster.html @@ -20,7 +20,7 @@ data-signals:_homepage="'{{ homepage }}'" data-on:submit="$_submitting = true; $_submitError = ''; @put('/api/v1/roasters/{{ id }}', {contentType: 'form'})" data-on:datastar-fetch="if (!$_submitting) return; - if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated'); window.location.href = evt.detail.response.headers.get('location') || '/roasters/{{ id }}' } + if (evt.detail.type === 'finished') { $_submitting = false; sessionStorage.setItem('toast', 'Roaster updated') } else if (evt.detail.type === 'error') { $_submitting = false; $_submitError = 'Failed to save changes.' }" >
@@ -81,7 +81,7 @@ />
- {{ img::deferred_upload("edit-roaster-image", "Roaster Image") }} + {{ img::deferred_upload_with_preview("edit-roaster-image", "Roaster Image", "roaster", id, image_url) }} -
+
+
diff --git a/templates/partials/image_section.html b/templates/partials/image_section.html index 5b972c4..06543a9 100644 --- a/templates/partials/image_section.html +++ b/templates/partials/image_section.html @@ -120,6 +120,60 @@ {% endmacro %} +{% macro deferred_upload_with_preview(input_id, label, entity_type, entity_id, image_url) %} + + {% if let Some(url) = image_url %} +
+ +
+ + Replace + + +
+
+ {% else %} + + + + + {{ label }} + + {% endif %} +{% endmacro %} + {% macro lightbox_script() %}