refactor: extract duplicated helpers for tasting notes, used_percent, format_datetime, and map builder
- Add parse_and_categorize() in tasting_notes.rs, replacing identical
split-trim-categorize pipelines in mod.rs, roasts.rs, and timeline.rs
- Add used_percent() in bags.rs, deduplicating the calculation in
BagView::from_domain and BagDetailView::from_parts
- Add format_datetime() in views/mod.rs, replacing 16 paired occurrences
of .format("%Y-%m-%d") / .format("%H:%M") across 7 view files
- Add build_origin_roaster_map() in views/mod.rs, replacing identical
map-entry building blocks in bags.rs, brews.rs, and roasts.rs detail views
This commit is contained in:
parent
3d49236c13
commit
766ac1432d
10 changed files with 125 additions and 132 deletions
|
|
@ -4,7 +4,17 @@ use crate::domain::roasters::Roaster;
|
||||||
use crate::domain::roasts::Roast;
|
use crate::domain::roasts::Roast;
|
||||||
|
|
||||||
use super::tasting_notes::TastingNoteView;
|
use super::tasting_notes::TastingNoteView;
|
||||||
use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info};
|
use super::{
|
||||||
|
LegendEntry, build_coffee_info, build_origin_roaster_map, build_roaster_info, format_datetime,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn used_percent(amount: f64, remaining: f64) -> u8 {
|
||||||
|
if amount > 0.0 {
|
||||||
|
(((amount - remaining) / amount) * 100.0).clamp(0.0, 100.0) as u8
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BagView {
|
pub struct BagView {
|
||||||
|
|
@ -26,12 +36,8 @@ pub struct BagView {
|
||||||
|
|
||||||
impl BagView {
|
impl BagView {
|
||||||
pub fn from_domain(bag: BagWithRoast) -> Self {
|
pub fn from_domain(bag: BagWithRoast) -> Self {
|
||||||
let used_percent = if bag.bag.amount > 0.0 {
|
let used_percent = used_percent(bag.bag.amount, bag.bag.remaining);
|
||||||
(((bag.bag.amount - bag.bag.remaining) / bag.bag.amount) * 100.0).clamp(0.0, 100.0)
|
let (created_date, created_time) = format_datetime(bag.bag.created_at);
|
||||||
as u8
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
id: bag.bag.id.to_string(),
|
id: bag.bag.id.to_string(),
|
||||||
roast_id: bag.bag.roast_id.to_string(),
|
roast_id: bag.bag.roast_id.to_string(),
|
||||||
|
|
@ -43,8 +49,8 @@ impl BagView {
|
||||||
.bag
|
.bag
|
||||||
.finished_at
|
.finished_at
|
||||||
.map_or_else(|| "—".to_string(), |d| d.format("%Y-%m-%d").to_string()),
|
.map_or_else(|| "—".to_string(), |d| d.format("%Y-%m-%d").to_string()),
|
||||||
created_date: bag.bag.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: bag.bag.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
roast_name: bag.roast_name,
|
roast_name: bag.roast_name,
|
||||||
roaster_name: bag.roaster_name,
|
roaster_name: bag.roaster_name,
|
||||||
roast_slug: bag.roast_slug,
|
roast_slug: bag.roast_slug,
|
||||||
|
|
@ -103,14 +109,9 @@ impl BagDetailView {
|
||||||
let coffee = build_coffee_info(roast);
|
let coffee = build_coffee_info(roast);
|
||||||
let roaster_info = build_roaster_info(roaster);
|
let roaster_info = build_roaster_info(roaster);
|
||||||
|
|
||||||
let mut map_entries: Vec<(&str, u32)> = Vec::new();
|
let (map_countries, map_max, legend_entries) =
|
||||||
if let Some(ref o) = roast.origin
|
build_origin_roaster_map(roast.origin.as_deref(), &roaster.country);
|
||||||
&& !o.is_empty()
|
let (created_date, created_time) = format_datetime(bag.bag.created_at);
|
||||||
{
|
|
||||||
map_entries.push((o.as_str(), 2));
|
|
||||||
}
|
|
||||||
map_entries.push((roaster.country.as_str(), 1));
|
|
||||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: bag.bag.id.to_string(),
|
id: bag.bag.id.to_string(),
|
||||||
|
|
@ -130,12 +131,7 @@ impl BagDetailView {
|
||||||
roaster_homepage: roaster_info.homepage,
|
roaster_homepage: roaster_info.homepage,
|
||||||
amount: format_weight(bag.bag.amount),
|
amount: format_weight(bag.bag.amount),
|
||||||
remaining: format_weight(bag.bag.remaining),
|
remaining: format_weight(bag.bag.remaining),
|
||||||
used_percent: if bag.bag.amount > 0.0 {
|
used_percent: used_percent(bag.bag.amount, bag.bag.remaining),
|
||||||
(((bag.bag.amount - bag.bag.remaining) / bag.bag.amount) * 100.0).clamp(0.0, 100.0)
|
|
||||||
as u8
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
closed: bag.bag.closed,
|
closed: bag.bag.closed,
|
||||||
roast_date: bag.bag.roast_date.map(|d| d.to_string()),
|
roast_date: bag.bag.roast_date.map(|d| d.to_string()),
|
||||||
finished_date: bag
|
finished_date: bag
|
||||||
|
|
@ -144,18 +140,9 @@ impl BagDetailView {
|
||||||
.map(|d| d.format("%Y-%m-%d").to_string()),
|
.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||||
map_countries,
|
map_countries,
|
||||||
map_max,
|
map_max,
|
||||||
legend_entries: vec![
|
legend_entries,
|
||||||
LegendEntry {
|
created_date,
|
||||||
label: "Origin",
|
created_time,
|
||||||
opacity: "",
|
|
||||||
},
|
|
||||||
LegendEntry {
|
|
||||||
label: "Roaster",
|
|
||||||
opacity: "opacity-50",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
created_date: bag.bag.created_at.format("%Y-%m-%d").to_string(),
|
|
||||||
created_time: bag.bag.created_at.format("%H:%M").to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ use crate::domain::roasters::Roaster;
|
||||||
use crate::domain::roasts::Roast;
|
use crate::domain::roasts::Roast;
|
||||||
|
|
||||||
use super::tasting_notes::TastingNoteView;
|
use super::tasting_notes::TastingNoteView;
|
||||||
use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info, relative_date};
|
use super::{
|
||||||
|
LegendEntry, build_coffee_info, build_origin_roaster_map, build_roaster_info, format_datetime,
|
||||||
|
relative_date,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct QuickNoteView {
|
pub struct QuickNoteView {
|
||||||
|
|
@ -92,6 +95,7 @@ impl BrewView {
|
||||||
.map(|n| n.form_value.as_str())
|
.map(|n| n.form_value.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
|
let (created_date, created_time) = format_datetime(brew.brew.created_at);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: brew.brew.id.to_string(),
|
id: brew.brew.id.to_string(),
|
||||||
|
|
@ -119,8 +123,8 @@ impl BrewView {
|
||||||
quick_notes,
|
quick_notes,
|
||||||
quick_notes_label,
|
quick_notes_label,
|
||||||
quick_notes_raw,
|
quick_notes_raw,
|
||||||
created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: brew.brew.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
relative_date_label: relative_date(brew.brew.created_at),
|
relative_date_label: relative_date(brew.brew.created_at),
|
||||||
coffee_weight_raw: brew.brew.coffee_weight,
|
coffee_weight_raw: brew.brew.coffee_weight,
|
||||||
grind_setting_raw: brew.brew.grind_setting,
|
grind_setting_raw: brew.brew.grind_setting,
|
||||||
|
|
@ -236,14 +240,9 @@ impl BrewDetailView {
|
||||||
let coffee = build_coffee_info(roast);
|
let coffee = build_coffee_info(roast);
|
||||||
let roaster_info = build_roaster_info(roaster);
|
let roaster_info = build_roaster_info(roaster);
|
||||||
|
|
||||||
let mut map_entries: Vec<(&str, u32)> = Vec::new();
|
let (map_countries, map_max, legend_entries) =
|
||||||
if let Some(ref o) = roast.origin
|
build_origin_roaster_map(roast.origin.as_deref(), &roaster.country);
|
||||||
&& !o.is_empty()
|
let (created_date, created_time) = format_datetime(brew.brew.created_at);
|
||||||
{
|
|
||||||
map_entries.push((o.as_str(), 2));
|
|
||||||
}
|
|
||||||
map_entries.push((roaster.country.as_str(), 1));
|
|
||||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
|
||||||
|
|
||||||
let quick_notes_label = brew
|
let quick_notes_label = brew
|
||||||
.brew
|
.brew
|
||||||
|
|
@ -280,18 +279,9 @@ impl BrewDetailView {
|
||||||
filter_paper_name: brew.filter_paper_name,
|
filter_paper_name: brew.filter_paper_name,
|
||||||
map_countries,
|
map_countries,
|
||||||
map_max,
|
map_max,
|
||||||
legend_entries: vec![
|
legend_entries,
|
||||||
LegendEntry {
|
created_date,
|
||||||
label: "Origin",
|
created_time,
|
||||||
opacity: "",
|
|
||||||
},
|
|
||||||
LegendEntry {
|
|
||||||
label: "Roaster",
|
|
||||||
opacity: "opacity-50",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(),
|
|
||||||
created_time: brew.brew.created_at.format("%H:%M").to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::domain::cafes::Cafe;
|
||||||
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
|
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
|
||||||
use crate::domain::nearby_cafes::NearbyCafeResult;
|
use crate::domain::nearby_cafes::NearbyCafeResult;
|
||||||
|
|
||||||
use super::{LegendEntry, build_map_data};
|
use super::{LegendEntry, build_map_data, format_datetime};
|
||||||
|
|
||||||
pub struct CafeDetailView {
|
pub struct CafeDetailView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -21,6 +21,7 @@ pub struct CafeDetailView {
|
||||||
|
|
||||||
impl CafeDetailView {
|
impl CafeDetailView {
|
||||||
pub fn from_domain(cafe: Cafe) -> Self {
|
pub fn from_domain(cafe: Cafe) -> Self {
|
||||||
|
let (created_date, created_time) = format_datetime(cafe.created_at);
|
||||||
let country_flag = country_to_iso(&cafe.country)
|
let country_flag = country_to_iso(&cafe.country)
|
||||||
.map(iso_to_flag_emoji)
|
.map(iso_to_flag_emoji)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -44,8 +45,8 @@ impl CafeDetailView {
|
||||||
label: "Cafe",
|
label: "Cafe",
|
||||||
opacity: "",
|
opacity: "",
|
||||||
}],
|
}],
|
||||||
created_date: cafe.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: cafe.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +93,7 @@ impl From<Cafe> for CafeView {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let created_at_sort_key = created_at.timestamp();
|
let created_at_sort_key = created_at.timestamp();
|
||||||
let created_date = created_at.format("%Y-%m-%d").to_string();
|
let (created_date, created_time) = format_datetime(created_at);
|
||||||
let created_time = created_at.format("%H:%M").to_string();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
detail_path,
|
detail_path,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::domain::roasters::Roaster;
|
||||||
use crate::domain::roasts::Roast;
|
use crate::domain::roasts::Roast;
|
||||||
|
|
||||||
use super::tasting_notes::TastingNoteView;
|
use super::tasting_notes::TastingNoteView;
|
||||||
use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info};
|
use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info, format_datetime};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CupView {
|
pub struct CupView {
|
||||||
|
|
@ -23,6 +23,7 @@ pub struct CupView {
|
||||||
|
|
||||||
impl CupView {
|
impl CupView {
|
||||||
pub fn from_domain(cup: CupWithDetails) -> Self {
|
pub fn from_domain(cup: CupWithDetails) -> Self {
|
||||||
|
let (created_date, created_time) = format_datetime(cup.cup.created_at);
|
||||||
Self {
|
Self {
|
||||||
id: cup.cup.id.to_string(),
|
id: cup.cup.id.to_string(),
|
||||||
roast_name: cup.roast_name,
|
roast_name: cup.roast_name,
|
||||||
|
|
@ -32,8 +33,8 @@ impl CupView {
|
||||||
cafe_name: cup.cafe_name,
|
cafe_name: cup.cafe_name,
|
||||||
cafe_slug: cup.cafe_slug,
|
cafe_slug: cup.cafe_slug,
|
||||||
cafe_city: cup.cafe_city,
|
cafe_city: cup.cafe_city,
|
||||||
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: cup.cup.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +93,7 @@ impl CupDetailView {
|
||||||
}
|
}
|
||||||
map_entries.push((roaster.country.as_str(), 1));
|
map_entries.push((roaster.country.as_str(), 1));
|
||||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
let (map_countries, map_max) = build_map_data(&map_entries);
|
||||||
|
let (created_date, created_time) = format_datetime(cup.cup.created_at);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: cup.cup.id.to_string(),
|
id: cup.cup.id.to_string(),
|
||||||
|
|
@ -135,8 +137,8 @@ impl CupDetailView {
|
||||||
opacity: "opacity-35",
|
opacity: "opacity-35",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: cup.cup.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::domain::gear::Gear;
|
use crate::domain::gear::Gear;
|
||||||
|
|
||||||
|
use super::format_datetime;
|
||||||
|
|
||||||
pub struct GearDetailView {
|
pub struct GearDetailView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub category_label: String,
|
pub category_label: String,
|
||||||
|
|
@ -11,13 +13,14 @@ pub struct GearDetailView {
|
||||||
|
|
||||||
impl GearDetailView {
|
impl GearDetailView {
|
||||||
pub fn from_domain(gear: Gear) -> Self {
|
pub fn from_domain(gear: Gear) -> Self {
|
||||||
|
let (created_date, created_time) = format_datetime(gear.created_at);
|
||||||
Self {
|
Self {
|
||||||
id: gear.id.to_string(),
|
id: gear.id.to_string(),
|
||||||
category_label: gear.category.display_label().to_string(),
|
category_label: gear.category.display_label().to_string(),
|
||||||
make: gear.make,
|
make: gear.make,
|
||||||
model: gear.model,
|
model: gear.model,
|
||||||
created_date: gear.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: gear.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +39,7 @@ pub struct GearView {
|
||||||
|
|
||||||
impl GearView {
|
impl GearView {
|
||||||
pub fn from_domain(gear: Gear) -> Self {
|
pub fn from_domain(gear: Gear) -> Self {
|
||||||
|
let (created_date, created_time) = format_datetime(gear.created_at);
|
||||||
Self {
|
Self {
|
||||||
id: gear.id.to_string(),
|
id: gear.id.to_string(),
|
||||||
category: gear.category.as_str().to_string(),
|
category: gear.category.as_str().to_string(),
|
||||||
|
|
@ -43,8 +47,8 @@ impl GearView {
|
||||||
make: gear.make.clone(),
|
make: gear.make.clone(),
|
||||||
model: gear.model.clone(),
|
model: gear.model.clone(),
|
||||||
full_name: format!("{} {}", gear.make, gear.model),
|
full_name: format!("{} {}", gear.make, gear.model),
|
||||||
created_date: gear.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: gear.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,13 @@ fn relative_date(dt: DateTime<Utc>) -> String {
|
||||||
crate::domain::formatting::format_relative_time(dt, Utc::now())
|
crate::domain::formatting::format_relative_time(dt, Utc::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_datetime(dt: DateTime<Utc>) -> (String, String) {
|
||||||
|
(
|
||||||
|
dt.format("%Y-%m-%d").to_string(),
|
||||||
|
dt.format("%H:%M").to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Paginated<T> {
|
pub struct Paginated<T> {
|
||||||
pub items: Vec<T>,
|
pub items: Vec<T>,
|
||||||
pub page: u32,
|
pub page: u32,
|
||||||
|
|
@ -388,17 +395,7 @@ pub(crate) fn build_coffee_info(roast: &crate::domain::roasts::Roast) -> CoffeeI
|
||||||
.map(iso_to_flag_emoji)
|
.map(iso_to_flag_emoji)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let notes = roast
|
let notes = tasting_notes::parse_and_categorize(&roast.tasting_notes);
|
||||||
.tasting_notes
|
|
||||||
.iter()
|
|
||||||
.flat_map(|note| {
|
|
||||||
note.split([',', '\n'])
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.map(|n| tasting_notes::categorize(&n))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
CoffeeInfo {
|
CoffeeInfo {
|
||||||
origin: if origin.is_empty() {
|
origin: if origin.is_empty() {
|
||||||
|
|
@ -450,6 +447,36 @@ pub(crate) fn build_roaster_info(roaster: &crate::domain::roasters::Roaster) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build origin + roaster country map data with standard legend entries.
|
||||||
|
///
|
||||||
|
/// Origin gets weight 2, roaster country gets weight 1. Returns the
|
||||||
|
/// `(data-countries, data-max, legend_entries)` tuple used by detail pages.
|
||||||
|
pub(crate) fn build_origin_roaster_map(
|
||||||
|
origin: Option<&str>,
|
||||||
|
roaster_country: &str,
|
||||||
|
) -> (String, u32, Vec<LegendEntry>) {
|
||||||
|
let mut entries: Vec<(&str, u32)> = Vec::new();
|
||||||
|
if let Some(o) = origin.filter(|o| !o.is_empty()) {
|
||||||
|
entries.push((o, 2));
|
||||||
|
}
|
||||||
|
entries.push((roaster_country, 1));
|
||||||
|
let (map_countries, map_max) = build_map_data(&entries);
|
||||||
|
(
|
||||||
|
map_countries,
|
||||||
|
map_max,
|
||||||
|
vec![
|
||||||
|
LegendEntry {
|
||||||
|
label: "Origin",
|
||||||
|
opacity: "",
|
||||||
|
},
|
||||||
|
LegendEntry {
|
||||||
|
label: "Roaster",
|
||||||
|
opacity: "opacity-50",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Build `data-countries` and `data-max` values for the world-map component.
|
/// Build `data-countries` and `data-max` values for the world-map component.
|
||||||
///
|
///
|
||||||
/// Accepts `(country_name, weight)` pairs where higher weights render darker.
|
/// Accepts `(country_name, weight)` pairs where higher weights render darker.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
|
use crate::domain::countries::{country_to_iso, iso_to_flag_emoji};
|
||||||
use crate::domain::roasters::Roaster;
|
use crate::domain::roasters::Roaster;
|
||||||
|
|
||||||
use super::{LegendEntry, build_map_data};
|
use super::{LegendEntry, build_map_data, format_datetime};
|
||||||
|
|
||||||
pub struct RoasterDetailView {
|
pub struct RoasterDetailView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -23,6 +23,7 @@ impl RoasterDetailView {
|
||||||
.map(iso_to_flag_emoji)
|
.map(iso_to_flag_emoji)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let (map_countries, map_max) = build_map_data(&[(&roaster.country, 1)]);
|
let (map_countries, map_max) = build_map_data(&[(&roaster.country, 1)]);
|
||||||
|
let (created_date, created_time) = format_datetime(roaster.created_at);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: roaster.id.to_string(),
|
id: roaster.id.to_string(),
|
||||||
|
|
@ -37,8 +38,8 @@ impl RoasterDetailView {
|
||||||
label: "Roaster",
|
label: "Roaster",
|
||||||
opacity: "",
|
opacity: "",
|
||||||
}],
|
}],
|
||||||
created_date: roaster.created_at.format("%Y-%m-%d").to_string(),
|
created_date,
|
||||||
created_time: roaster.created_at.format("%H:%M").to_string(),
|
created_time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,8 +102,7 @@ impl From<Roaster> for RoasterView {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let created_at_sort_key = created_at.timestamp();
|
let created_at_sort_key = created_at.timestamp();
|
||||||
let created_date = created_at.format("%Y-%m-%d").to_string();
|
let (created_date, created_time) = format_datetime(created_at);
|
||||||
let created_time = created_at.format("%H:%M").to_string();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
detail_path,
|
detail_path,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ use crate::domain::roasters::Roaster;
|
||||||
use crate::domain::roasts::{Roast, RoastWithRoaster};
|
use crate::domain::roasts::{Roast, RoastWithRoaster};
|
||||||
|
|
||||||
use super::tasting_notes::{self, TastingNoteView};
|
use super::tasting_notes::{self, TastingNoteView};
|
||||||
use super::{LegendEntry, build_coffee_info, build_map_data, build_roaster_info};
|
use super::{
|
||||||
|
LegendEntry, build_coffee_info, build_origin_roaster_map, build_roaster_info, format_datetime,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct RoastView {
|
pub struct RoastView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -67,18 +69,8 @@ impl RoastView {
|
||||||
let producer = producer.unwrap_or_else(|| "—".to_string());
|
let producer = producer.unwrap_or_else(|| "—".to_string());
|
||||||
let process = process.unwrap_or_else(|| "—".to_string());
|
let process = process.unwrap_or_else(|| "—".to_string());
|
||||||
let created_at_sort_key = created_at.timestamp();
|
let created_at_sort_key = created_at.timestamp();
|
||||||
let tasting_notes = tasting_notes
|
let tasting_notes = tasting_notes::parse_and_categorize(&tasting_notes);
|
||||||
.into_iter()
|
let (created_date, created_time) = format_datetime(created_at);
|
||||||
.flat_map(|note| {
|
|
||||||
note.split([',', '\n'])
|
|
||||||
.map(|segment| segment.trim().to_string())
|
|
||||||
.filter(|segment| !segment.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.map(|note| tasting_notes::categorize(¬e))
|
|
||||||
.collect();
|
|
||||||
let created_date = created_at.format("%Y-%m-%d").to_string();
|
|
||||||
let created_time = created_at.format("%H:%M").to_string();
|
|
||||||
let detail_path = format!("/roasters/{roaster_slug}/roasts/{slug}");
|
let detail_path = format!("/roasters/{roaster_slug}/roasts/{slug}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -131,14 +123,9 @@ impl RoastDetailView {
|
||||||
let coffee = build_coffee_info(&roast);
|
let coffee = build_coffee_info(&roast);
|
||||||
let roaster_info = build_roaster_info(roaster);
|
let roaster_info = build_roaster_info(roaster);
|
||||||
|
|
||||||
let mut map_entries: Vec<(&str, u32)> = Vec::new();
|
let (map_countries, map_max, legend_entries) =
|
||||||
if let Some(ref o) = roast.origin
|
build_origin_roaster_map(roast.origin.as_deref(), &roaster.country);
|
||||||
&& !o.is_empty()
|
let (created_date, created_time) = format_datetime(roast.created_at);
|
||||||
{
|
|
||||||
map_entries.push((o.as_str(), 2));
|
|
||||||
}
|
|
||||||
map_entries.push((roaster.country.as_str(), 1));
|
|
||||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: roast.id.to_string(),
|
id: roast.id.to_string(),
|
||||||
|
|
@ -157,18 +144,9 @@ impl RoastDetailView {
|
||||||
roaster_homepage: roaster_info.homepage,
|
roaster_homepage: roaster_info.homepage,
|
||||||
map_countries,
|
map_countries,
|
||||||
map_max,
|
map_max,
|
||||||
legend_entries: vec![
|
legend_entries,
|
||||||
LegendEntry {
|
created_date,
|
||||||
label: "Origin",
|
created_time,
|
||||||
opacity: "",
|
|
||||||
},
|
|
||||||
LegendEntry {
|
|
||||||
label: "Roaster",
|
|
||||||
opacity: "opacity-50",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
created_date: roast.created_at.format("%Y-%m-%d").to_string(),
|
|
||||||
created_time: roast.created_at.format("%H:%M").to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,21 @@ pub fn categorize(note: &str) -> TastingNoteView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split raw tasting note strings on commas and newlines, trim whitespace,
|
||||||
|
/// drop empties, and categorise each resulting segment.
|
||||||
|
pub fn parse_and_categorize(notes: &[String]) -> Vec<TastingNoteView> {
|
||||||
|
notes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|note| {
|
||||||
|
note.split([',', '\n'])
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.map(|n| categorize(&n))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Exact matches ────────────────────────────────────────────────────
|
// ── Exact matches ────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn exact_match(lower: &str) -> Option<NoteCategory> {
|
fn exact_match(lower: &str) -> Option<NoteCategory> {
|
||||||
|
|
|
||||||
|
|
@ -125,17 +125,7 @@ impl TimelineEventView {
|
||||||
Self::add_country_flags(&mut mapped_details);
|
Self::add_country_flags(&mut mapped_details);
|
||||||
|
|
||||||
let tasting_notes = if entity_type == EntityType::Roast {
|
let tasting_notes = if entity_type == EntityType::Roast {
|
||||||
let notes = tasting_notes
|
Some(tasting_notes::parse_and_categorize(&tasting_notes))
|
||||||
.into_iter()
|
|
||||||
.flat_map(|note| {
|
|
||||||
note.split([',', '\n'])
|
|
||||||
.map(|segment| segment.trim().to_string())
|
|
||||||
.filter(|segment| !segment.is_empty())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.map(|note| tasting_notes::categorize(¬e))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Some(notes)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue