feat(ui): add entity links to detail page cards
Add roaster and roast slug parameters to coffee_card and roaster_card macros, rendering entity names as links to their detail pages. Add cafe link in the cup detail page. Pass slug fields through template structs and route handlers.
This commit is contained in:
parent
3c273feb18
commit
da206c1938
12 changed files with 78 additions and 15 deletions
|
|
@ -44,6 +44,8 @@ pub(crate) async fn bag_detail_page(
|
||||||
version_info: &crate::VERSION_INFO,
|
version_info: &crate::VERSION_INFO,
|
||||||
base_url: crate::base_url(),
|
base_url: crate::base_url(),
|
||||||
bag: view,
|
bag: view,
|
||||||
|
roaster_slug: roaster.slug.clone(),
|
||||||
|
roast_slug: roast.slug.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
render_html(template).map(IntoResponse::into_response)
|
render_html(template).map(IntoResponse::into_response)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ pub(crate) async fn brew_detail_page(
|
||||||
version_info: &crate::VERSION_INFO,
|
version_info: &crate::VERSION_INFO,
|
||||||
base_url: crate::base_url(),
|
base_url: crate::base_url(),
|
||||||
brew: view,
|
brew: view,
|
||||||
|
roaster_slug: roaster.slug.clone(),
|
||||||
|
roast_slug: roast.slug.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
render_html(template).map(IntoResponse::into_response)
|
render_html(template).map(IntoResponse::into_response)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ pub(crate) async fn cup_detail_page(
|
||||||
version_info: &crate::VERSION_INFO,
|
version_info: &crate::VERSION_INFO,
|
||||||
base_url: crate::base_url(),
|
base_url: crate::base_url(),
|
||||||
cup: view,
|
cup: view,
|
||||||
|
roaster_slug: roaster.slug.clone(),
|
||||||
|
roast_slug: roast.slug.clone(),
|
||||||
|
cafe_slug: cafe.slug.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
render_html(template).map(IntoResponse::into_response)
|
render_html(template).map(IntoResponse::into_response)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ pub(crate) async fn roast_detail_page(
|
||||||
version_info: &crate::VERSION_INFO,
|
version_info: &crate::VERSION_INFO,
|
||||||
base_url: crate::base_url(),
|
base_url: crate::base_url(),
|
||||||
roast: view,
|
roast: view,
|
||||||
|
roaster_slug,
|
||||||
};
|
};
|
||||||
|
|
||||||
render_html(template).map(IntoResponse::into_response)
|
render_html(template).map(IntoResponse::into_response)
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,8 @@ pub struct BagDetailTemplate {
|
||||||
pub version_info: &'static crate::VersionInfo,
|
pub version_info: &'static crate::VersionInfo,
|
||||||
pub base_url: &'static str,
|
pub base_url: &'static str,
|
||||||
pub bag: BagDetailView,
|
pub bag: BagDetailView,
|
||||||
|
pub roaster_slug: String,
|
||||||
|
pub roast_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
@ -229,6 +231,8 @@ pub struct BrewDetailTemplate {
|
||||||
pub version_info: &'static crate::VersionInfo,
|
pub version_info: &'static crate::VersionInfo,
|
||||||
pub base_url: &'static str,
|
pub base_url: &'static str,
|
||||||
pub brew: BrewDetailView,
|
pub brew: BrewDetailView,
|
||||||
|
pub roaster_slug: String,
|
||||||
|
pub roast_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
@ -239,6 +243,9 @@ pub struct CupDetailTemplate {
|
||||||
pub version_info: &'static crate::VersionInfo,
|
pub version_info: &'static crate::VersionInfo,
|
||||||
pub base_url: &'static str,
|
pub base_url: &'static str,
|
||||||
pub cup: CupDetailView,
|
pub cup: CupDetailView,
|
||||||
|
pub roaster_slug: String,
|
||||||
|
pub roast_slug: String,
|
||||||
|
pub cafe_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
@ -249,6 +256,7 @@ pub struct RoastDetailTemplate {
|
||||||
pub version_info: &'static crate::VersionInfo,
|
pub version_info: &'static crate::VersionInfo,
|
||||||
pub base_url: &'static str,
|
pub base_url: &'static str,
|
||||||
pub roast: RoastDetailView,
|
pub roast: RoastDetailView,
|
||||||
|
pub roaster_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ pub struct BagDetailView {
|
||||||
// Map
|
// Map
|
||||||
pub map_countries: String,
|
pub map_countries: String,
|
||||||
pub map_max: u32,
|
pub map_max: u32,
|
||||||
|
// Slugs (for breadcrumbs)
|
||||||
|
pub roaster_slug: String,
|
||||||
|
pub roast_slug: String,
|
||||||
// Dates
|
// Dates
|
||||||
pub created_date: String,
|
pub created_date: String,
|
||||||
pub created_time: String,
|
pub created_time: String,
|
||||||
|
|
@ -112,6 +115,8 @@ impl BagDetailView {
|
||||||
id: bag.bag.id.to_string(),
|
id: bag.bag.id.to_string(),
|
||||||
roast_name: bag.roast_name,
|
roast_name: bag.roast_name,
|
||||||
roaster_name: bag.roaster_name,
|
roaster_name: bag.roaster_name,
|
||||||
|
roaster_slug: roaster.slug.clone(),
|
||||||
|
roast_slug: roast.slug.clone(),
|
||||||
origin: coffee.origin,
|
origin: coffee.origin,
|
||||||
origin_flag: coffee.origin_flag,
|
origin_flag: coffee.origin_flag,
|
||||||
region: coffee.region,
|
region: coffee.region,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ pub struct CupDetailView {
|
||||||
// Map
|
// Map
|
||||||
pub map_countries: String,
|
pub map_countries: String,
|
||||||
pub map_max: u32,
|
pub map_max: u32,
|
||||||
|
// Slugs (for breadcrumbs)
|
||||||
|
pub roaster_slug: String,
|
||||||
|
pub roast_slug: String,
|
||||||
|
pub cafe_slug: String,
|
||||||
// Dates
|
// Dates
|
||||||
pub created_date: String,
|
pub created_date: String,
|
||||||
pub created_time: String,
|
pub created_time: String,
|
||||||
|
|
@ -106,6 +110,9 @@ impl CupDetailView {
|
||||||
cafe_country: cafe.country.clone(),
|
cafe_country: cafe.country.clone(),
|
||||||
cafe_country_flag,
|
cafe_country_flag,
|
||||||
cafe_website: cafe.website.clone(),
|
cafe_website: cafe.website.clone(),
|
||||||
|
roaster_slug: roaster.slug.clone(),
|
||||||
|
roast_slug: roast.slug.clone(),
|
||||||
|
cafe_slug: cafe.slug.clone(),
|
||||||
map_countries,
|
map_countries,
|
||||||
map_max,
|
map_max,
|
||||||
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
|
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::coffee_card(bag.roast_name, bag.roaster_name, bag.origin, bag.origin_flag, bag.region, bag.producer, bag.process, bag.tasting_notes) }}
|
{{ 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_2(bag.map_countries, bag.map_max, "Origin", "", "Roaster", "opacity-50") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Roaster & bag info ── #}
|
{# ── Roaster & bag info ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::roaster_card(bag.roaster_name, bag.roaster_country, bag.roaster_country_flag, bag.roaster_city, bag.roaster_homepage) }}
|
{{ detail::roaster_card(bag.roaster_name, bag.roaster_country, bag.roaster_country_flag, bag.roaster_city, bag.roaster_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">Bag</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Bag</h2>
|
||||||
|
|
@ -49,7 +49,14 @@
|
||||||
<span class="font-medium text-text">Closed</span>
|
<span class="font-medium text-text">Closed</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div class="h-2 rounded-full bg-surface-alt overflow-hidden">
|
<div
|
||||||
|
class="h-2 rounded-full bg-surface-alt overflow-hidden"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow="{{ bag.used_percent }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
aria-label="Coffee used"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full bg-accent"
|
class="h-full rounded-full bg-accent"
|
||||||
style="width: {{ bag.used_percent }}%"
|
style="width: {{ bag.used_percent }}%"
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,13 @@
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::coffee_card(brew.roast_name, brew.roaster_name, brew.origin, brew.origin_flag, brew.region, brew.producer, brew.process, brew.tasting_notes) }}
|
{{ 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_2(brew.map_countries, brew.map_max, "Origin", "", "Roaster", "opacity-50") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Roaster & gear ── #}
|
{# ── Roaster & gear ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::roaster_card(brew.roaster_name, brew.roaster_country, brew.roaster_country_flag, brew.roaster_city, brew.roaster_homepage) }}
|
{{ detail::roaster_card(brew.roaster_name, brew.roaster_country, brew.roaster_country_flag, brew.roaster_city, brew.roaster_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">Gear</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Gear</h2>
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,26 @@
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::coffee_card(cup.roast_name, cup.roaster_name, cup.origin, cup.origin_flag, cup.region, cup.producer, cup.process, cup.tasting_notes) }}
|
{{ 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_3(cup.map_countries, cup.map_max, "Cafe", "", "Origin", "opacity-65", "Roaster", "opacity-35") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Roaster & cafe ── #}
|
{# ── Roaster & cafe ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::roaster_card(cup.roaster_name, cup.roaster_country, cup.roaster_country_flag, cup.roaster_city, cup.roaster_homepage) }}
|
{{ detail::roaster_card(cup.roaster_name, cup.roaster_country, cup.roaster_country_flag, cup.roaster_city, cup.roaster_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">Cafe</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Cafe</h2>
|
||||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Name</dt>
|
<dt class="text-text-muted">Name</dt>
|
||||||
<dd class="font-medium text-text">{{ cup.cafe_name }}</dd>
|
<dd class="font-medium">
|
||||||
|
<a
|
||||||
|
href="/cafes/{{ cafe_slug }}"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>{{ cup.cafe_name }}</a
|
||||||
|
>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Country</dt>
|
<dt class="text-text-muted">Country</dt>
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::coffee_card(roast.name, roast.roaster_name, roast.origin, roast.origin_flag, roast.region, roast.producer, roast.process, roast.tasting_notes) }}
|
{{ 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_2(roast.map_countries, roast.map_max, "Origin", "", "Roaster", "opacity-50") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{{ detail::roaster_card(roast.roaster_name, roast.roaster_country, roast.roaster_country_flag, roast.roaster_city, roast.roaster_homepage) }}
|
{{ detail::roaster_card(roast.roaster_name, roast.roaster_country, roast.roaster_country_flag, roast.roaster_city, roast.roaster_homepage, roaster_slug) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,33 @@
|
||||||
</script>
|
</script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro coffee_card(roast_name, roaster_name, origin, origin_flag, region, producer, process, tasting_notes) %}
|
{% macro coffee_card(roast_name, roaster_name, origin, origin_flag, region, producer, process, tasting_notes, roaster_slug, roast_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">Coffee</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Coffee</h2>
|
||||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Roast</dt>
|
<dt class="text-text-muted">Roast</dt>
|
||||||
<dd class="font-medium text-text">{{ roast_name }}</dd>
|
<dd class="font-medium">
|
||||||
|
{% if !roast_slug.is_empty() %}
|
||||||
|
<a
|
||||||
|
href="/roasters/{{ roaster_slug }}/roasts/{{ roast_slug }}"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>{{ roast_name }}</a
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-text">{{ roast_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Roaster</dt>
|
<dt class="text-text-muted">Roaster</dt>
|
||||||
<dd class="font-medium text-text">{{ roaster_name }}</dd>
|
<dd class="font-medium">
|
||||||
|
<a
|
||||||
|
href="/roasters/{{ roaster_slug }}"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>{{ roaster_name }}</a
|
||||||
|
>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{% if origin != "\u{2014}" %}
|
{% if origin != "\u{2014}" %}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -169,13 +185,19 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro roaster_card(name, country, country_flag, city, homepage) %}
|
{% 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>
|
||||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Name</dt>
|
<dt class="text-text-muted">Name</dt>
|
||||||
<dd class="font-medium text-text">{{ name }}</dd>
|
<dd class="font-medium">
|
||||||
|
<a
|
||||||
|
href="/roasters/{{ roaster_slug }}"
|
||||||
|
class="text-accent hover:text-accent-hover transition"
|
||||||
|
>{{ name }}</a
|
||||||
|
>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-text-muted">Country</dt>
|
<dt class="text-text-muted">Country</dt>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue