feat: add edit button to all detail pages

Add edit_button and edit_delete_buttons macros to detail_cards.html.
All 7 entity detail pages now show an Edit button next to Delete when
authenticated. Each detail template struct receives a pre-computed
edit_url from the route handler.
This commit is contained in:
Jon Seager 2026-02-10 18:18:53 +00:00
parent 1c029f52f4
commit 94088f1f4b
No known key found for this signature in database
16 changed files with 51 additions and 6 deletions

View file

@ -46,6 +46,7 @@ pub(crate) async fn bag_detail_page(
is_authenticated, is_authenticated,
version_info: &crate::VERSION_INFO, version_info: &crate::VERSION_INFO,
base_url: crate::base_url(), base_url: crate::base_url(),
edit_url: format!("/bags/{id}/edit"),
bag: view, bag: view,
roaster_slug: roaster.slug.clone(), roaster_slug: roaster.slug.clone(),
roast_slug: roast.slug.clone(), roast_slug: roast.slug.clone(),

View file

@ -54,6 +54,7 @@ pub(crate) async fn brew_detail_page(
is_authenticated, is_authenticated,
version_info: &crate::VERSION_INFO, version_info: &crate::VERSION_INFO,
base_url: crate::base_url(), base_url: crate::base_url(),
edit_url: format!("/brews/{id}/edit"),
brew: view, brew: view,
roaster_slug: roaster.slug.clone(), roaster_slug: roaster.slug.clone(),
roast_slug: roast.slug.clone(), roast_slug: roast.slug.clone(),

View file

@ -25,6 +25,7 @@ pub(crate) async fn cafe_detail_page(
.map_err(|e| map_app_error(e.into()))?; .map_err(|e| map_app_error(e.into()))?;
let image_url = resolve_image_url(&state, "cafe", i64::from(cafe.id)).await; let image_url = resolve_image_url(&state, "cafe", i64::from(cafe.id)).await;
let edit_url = format!("/cafes/{}/edit", cafe.id);
let view = CafeDetailView::from_domain(cafe); let view = CafeDetailView::from_domain(cafe);
@ -33,6 +34,7 @@ pub(crate) async fn cafe_detail_page(
is_authenticated, is_authenticated,
version_info: &crate::VERSION_INFO, version_info: &crate::VERSION_INFO,
base_url: crate::base_url(), base_url: crate::base_url(),
edit_url,
cafe: view, cafe: view,
image_url, image_url,
}; };

View file

@ -60,6 +60,7 @@ pub(crate) async fn cup_detail_page(
is_authenticated, is_authenticated,
version_info: &crate::VERSION_INFO, version_info: &crate::VERSION_INFO,
base_url: crate::base_url(), base_url: crate::base_url(),
edit_url: format!("/cups/{id}/edit"),
cup: view, cup: view,
roaster_slug: roaster.slug.clone(), roaster_slug: roaster.slug.clone(),
roast_slug: roast.slug.clone(), roast_slug: roast.slug.clone(),

View file

@ -34,6 +34,7 @@ pub(crate) async fn gear_detail_page(
is_authenticated, is_authenticated,
version_info: &crate::VERSION_INFO, version_info: &crate::VERSION_INFO,
base_url: crate::base_url(), base_url: crate::base_url(),
edit_url: format!("/gear/{id}/edit"),
gear: view, gear: view,
image_url, image_url,
}; };

View file

@ -25,6 +25,7 @@ pub(crate) async fn roaster_detail_page(
.map_err(|e| map_app_error(e.into()))?; .map_err(|e| map_app_error(e.into()))?;
let image_url = resolve_image_url(&state, "roaster", i64::from(roaster.id)).await; let image_url = resolve_image_url(&state, "roaster", i64::from(roaster.id)).await;
let edit_url = format!("/roasters/{}/edit", roaster.id);
let view = RoasterDetailView::from_domain(roaster); let view = RoasterDetailView::from_domain(roaster);
@ -35,6 +36,7 @@ pub(crate) async fn roaster_detail_page(
base_url: crate::base_url(), base_url: crate::base_url(),
roaster: view, roaster: view,
image_url, image_url,
edit_url,
}; };
render_html(template).map(IntoResponse::into_response) render_html(template).map(IntoResponse::into_response)

View file

@ -31,6 +31,7 @@ pub(crate) async fn roast_detail_page(
.map_err(|e| map_app_error(e.into()))?; .map_err(|e| map_app_error(e.into()))?;
let image_url = resolve_image_url(&state, "roast", i64::from(roast.id)).await; let image_url = resolve_image_url(&state, "roast", i64::from(roast.id)).await;
let edit_url = format!("/roasts/{}/edit", roast.id);
let view = RoastDetailView::from_parts(roast, &roaster); let view = RoastDetailView::from_parts(roast, &roaster);
@ -42,6 +43,7 @@ pub(crate) async fn roast_detail_page(
roast: view, roast: view,
roaster_slug, roaster_slug,
image_url, image_url,
edit_url,
}; };
render_html(template).map(IntoResponse::into_response) render_html(template).map(IntoResponse::into_response)

View file

@ -222,6 +222,7 @@ pub struct BagDetailTemplate {
pub roaster_slug: String, pub roaster_slug: String,
pub roast_slug: String, pub roast_slug: String,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -235,6 +236,7 @@ pub struct BrewDetailTemplate {
pub roaster_slug: String, pub roaster_slug: String,
pub roast_slug: String, pub roast_slug: String,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -249,6 +251,7 @@ pub struct CupDetailTemplate {
pub roast_slug: String, pub roast_slug: String,
pub cafe_slug: String, pub cafe_slug: String,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -261,6 +264,7 @@ pub struct RoastDetailTemplate {
pub roast: RoastDetailView, pub roast: RoastDetailView,
pub roaster_slug: String, pub roaster_slug: String,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -272,6 +276,7 @@ pub struct RoasterDetailTemplate {
pub base_url: &'static str, pub base_url: &'static str,
pub roaster: RoasterDetailView, pub roaster: RoasterDetailView,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -283,6 +288,7 @@ pub struct CafeDetailTemplate {
pub base_url: &'static str, pub base_url: &'static str,
pub cafe: CafeDetailView, pub cafe: CafeDetailView,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -294,6 +300,7 @@ pub struct GearDetailTemplate {
pub base_url: &'static str, pub base_url: &'static str,
pub gear: GearDetailView, pub gear: GearDetailView,
pub image_url: Option<String>, pub image_url: Option<String>,
pub edit_url: String,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -114,6 +114,7 @@
<div <div
class="rounded-lg border bg-surface p-5 md:col-span-2 flex items-center gap-2" class="rounded-lg border bg-surface p-5 md:col-span-2 flex items-center gap-2"
> >
{{ detail::edit_button(edit_url) }}
{% if !bag.closed %} {% if !bag.closed %}
<button <button
type="button" type="button"

View file

@ -107,6 +107,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("brew", "/api/v1/brews", brew.id) }} {{ detail::edit_delete_buttons(edit_url, "brew", "/api/v1/brews", brew.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -65,6 +65,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("cafe", "/api/v1/cafes", cafe.id) }} {{ detail::edit_delete_buttons(edit_url, "cafe", "/api/v1/cafes", cafe.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -88,6 +88,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("cup", "/api/v1/cups", cup.id) }} {{ detail::edit_delete_buttons(edit_url, "cup", "/api/v1/cups", cup.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -52,6 +52,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("gear", "/api/v1/gear", gear.id) }} {{ detail::edit_delete_buttons(edit_url, "gear", "/api/v1/gear", gear.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -40,6 +40,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("roast", "/api/v1/roasts", roast.id) }} {{ detail::edit_delete_buttons(edit_url, "roast", "/api/v1/roasts", roast.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -72,6 +72,6 @@
</div> </div>
{% if is_authenticated %} {% if is_authenticated %}
{{ detail::delete_button("roaster", "/api/v1/roasters", roaster.id) }} {{ detail::edit_delete_buttons(edit_url, "roaster", "/api/v1/roasters", roaster.id) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -125,6 +125,15 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro edit_button(edit_url) %}
<a
href="{{ edit_url }}"
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-accent transition hover:bg-surface-alt"
>
{{ icons::pencil("h-4 w-4") }} Edit
</a>
{% endmacro %}
{% macro delete_button(entity_label, api_path, id) %} {% macro delete_button(entity_label, api_path, id) %}
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2"> <div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
<button <button
@ -137,6 +146,24 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro edit_delete_buttons(edit_url, entity_label, api_path, id) %}
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
<a
href="{{ edit_url }}"
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-accent transition hover:bg-surface-alt"
>
{{ icons::pencil("h-4 w-4") }} Edit
</a>
<button
type="button"
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-error transition hover:bg-surface-alt"
data-on:click="confirm('Delete this {{ entity_label }}? This cannot be undone.') && @delete('{{ api_path }}/{{ id }}')"
>
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
{% endmacro %}
{% macro roaster_card(name, country, country_flag, city, homepage, roaster_slug) %} {% macro roaster_card(name, country, country_flag, city, homepage, roaster_slug) %}
<div class="rounded-lg border bg-surface p-5"> <div class="rounded-lg border bg-surface p-5">
<h2 class="text-lg font-semibold text-text mb-4">Roaster</h2> <h2 class="text-lg font-semibold text-text mb-4">Roaster</h2>