feat(timeline): add country flags to detail rows, allow multiple cards open

- Add flag emojis to Origin and Country detail rows in timeline events
- Change expanded-card signal from single-value to comma-separated list
  so multiple timeline cards can be open simultaneously
- Build subtitle before adding flags to keep condensed text clean
This commit is contained in:
Jon Seager 2026-02-08 18:04:31 +00:00
parent 639d748384
commit a9fd069a3c
No known key found for this signature in database
2 changed files with 26 additions and 11 deletions

View file

@ -1,3 +1,4 @@
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
use crate::domain::timeline::{TimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{TimelineEvent, TimelineEventDetail};
use super::relative_date; use super::relative_date;
@ -114,7 +115,12 @@ impl TimelineEventView {
_ => String::from("#"), _ => String::from("#"),
}; };
let (mapped_details, external_link) = Self::map_details(details); let (mut mapped_details, external_link) = Self::map_details(details);
// Build subtitle before adding flags so it stays clean text.
let subtitle = Self::build_subtitle(entity_type.as_str(), &mapped_details);
Self::add_country_flags(&mut mapped_details);
let tasting_notes = if entity_type == "roast" { let tasting_notes = if entity_type == "roast" {
let notes = tasting_notes let notes = tasting_notes
@ -132,8 +138,6 @@ impl TimelineEventView {
None None
}; };
let subtitle = Self::build_subtitle(entity_type.as_str(), &mapped_details);
let brew_data_view = brew_data.map(|bd| TimelineBrewDataView { let brew_data_view = brew_data.map(|bd| TimelineBrewDataView {
bag_id: bd.bag_id, bag_id: bd.bag_id,
grinder_id: bd.grinder_id, grinder_id: bd.grinder_id,
@ -247,4 +251,15 @@ impl TimelineEventView {
(mapped, external_link) (mapped, external_link)
} }
fn add_country_flags(details: &mut [TimelineEventDetailView]) {
for detail in details.iter_mut() {
let label_lower = detail.label.to_ascii_lowercase();
if matches!(label_lower.as_str(), "origin" | "country")
&& let Some(flag) = country_to_iso(detail.value.trim()).map(iso_to_flag_emoji)
{
detail.value = format!("{flag} {}", detail.value);
}
}
}
} }

View file

@ -22,7 +22,7 @@
></span> ></span>
<div <div
class="tl-card rounded-lg border bg-surface p-5 text-left" class="tl-card rounded-lg border bg-surface p-5 text-left"
data-on:click="if (!evt.target.closest('a, button, form')) { if ($_expandedMonths.includes(',{{ month.anchor }}')) { $_collapsedCards = $_collapsedCards.includes(',{{ event.id }}') ? $_collapsedCards.replace(',{{ event.id }}', '') : $_collapsedCards + ',{{ event.id }}' } else { $_expandedCard = $_expandedCard === '{{ event.id }}' ? '' : '{{ event.id }}' } }" data-on:click="if (!evt.target.closest('a, button, form')) { if ($_expandedMonths.includes(',{{ month.anchor }}')) { $_collapsedCards = $_collapsedCards.includes(',{{ event.id }}') ? $_collapsedCards.replace(',{{ event.id }}', '') : $_collapsedCards + ',{{ event.id }}' } else { $_expandedCard = $_expandedCard.includes(',{{ event.id }}') ? $_expandedCard.replace(',{{ event.id }}', '') : $_expandedCard + ',{{ event.id }}' } }"
> >
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
{# Category — always visible #} {# Category — always visible #}
@ -30,20 +30,20 @@
{% if event.entity_type == "brew" %}{{ icons::beaker("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-3 w-3 shrink-0") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-3 w-3 shrink-0") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-3 w-3 shrink-0") }}{% endif %} {% if event.entity_type == "brew" %}{{ icons::beaker("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-3 w-3 shrink-0") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-3 w-3 shrink-0") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-3 w-3 shrink-0") }}{% endif %}
<span class="uppercase tracking-wide">{{ event.kind_label }}</span> <span class="uppercase tracking-wide">{{ event.kind_label }}</span>
{# Relative date — shown when collapsed #} {# Relative date — shown when collapsed #}
<span class="tl-condensed uppercase tracking-wide" data-show="($_expandedCard !== '{{ event.id }}' && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')"> · {{ event.relative_date_label }}</span> <span class="tl-condensed uppercase tracking-wide" data-show="(!$_expandedCard.includes(',{{ event.id }}') && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')"> · {{ event.relative_date_label }}</span>
{# Full timestamp — shown when expanded #} {# Full timestamp — shown when expanded #}
<time <time
datetime="{{ event.iso_timestamp }}" datetime="{{ event.iso_timestamp }}"
class="tl-detail uppercase tracking-wide" class="tl-detail uppercase tracking-wide"
data-show="($_expandedCard === '{{ event.id }}' || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')" data-show="($_expandedCard.includes(',{{ event.id }}') || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')"
style="display:none" style="display:none"
> · {{ event.date_label }}{% if let Some(label) = event.time_label %} · {{ label }}{% endif %}</time> > · {{ event.date_label }}{% if let Some(label) = event.time_label %} · {{ label }}{% endif %}</time>
</span> </span>
{# Expand/collapse chevron #} {# Expand/collapse chevron #}
<span class="tl-chevron text-text-muted" data-show="($_expandedCard !== '{{ event.id }}' && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')"> <span class="tl-chevron text-text-muted" data-show="(!$_expandedCard.includes(',{{ event.id }}') && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')">
{{ icons::chevron_down("h-4 w-4") }} {{ icons::chevron_down("h-4 w-4") }}
</span> </span>
<span class="tl-chevron text-text-muted" data-show="($_expandedCard === '{{ event.id }}' || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')" style="display:none"> <span class="tl-chevron text-text-muted" data-show="($_expandedCard.includes(',{{ event.id }}') || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')" style="display:none">
{{ icons::chevron_up("h-4 w-4") }} {{ icons::chevron_up("h-4 w-4") }}
</span> </span>
</div> </div>
@ -54,7 +54,7 @@
<a <a
href="{{ url }}" href="{{ url }}"
class="tl-detail ml-auto text-accent transition hover:text-accent-hover" class="tl-detail ml-auto text-accent transition hover:text-accent-hover"
data-show="($_expandedCard === '{{ event.id }}' || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')" data-show="($_expandedCard.includes(',{{ event.id }}') || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')"
style="display:none" style="display:none"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
@ -69,14 +69,14 @@
{% if let Some(sub) = event.subtitle %} {% if let Some(sub) = event.subtitle %}
<p <p
class="tl-condensed mt-1 text-sm text-text-muted" class="tl-condensed mt-1 text-sm text-text-muted"
data-show="($_expandedCard !== '{{ event.id }}' && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')" data-show="(!$_expandedCard.includes(',{{ event.id }}') && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')"
>{{ sub }}</p> >{{ sub }}</p>
{% endif %} {% endif %}
{# Detail rows — shown when expanded #} {# Detail rows — shown when expanded #}
{% if event.details.len() > 0 %} {% if event.details.len() > 0 %}
<dl <dl
class="tl-detail mt-4 flex flex-col gap-2 text-sm text-text-secondary" class="tl-detail mt-4 flex flex-col gap-2 text-sm text-text-secondary"
data-show="($_expandedCard === '{{ event.id }}' || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')" data-show="($_expandedCard.includes(',{{ event.id }}') || $_expandedMonths.includes(',{{ month.anchor }}')) && !$_collapsedCards.includes(',{{ event.id }}')"
style="display:none" style="display:none"
> >
{% for detail in event.details %} {% for detail in event.details %}