From 3a960b9d1449f2a5b55cf54581b9ebfb51f24f85 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Fri, 6 Feb 2026 15:40:40 +0000 Subject: [PATCH] feat(ui): enhance desktop tables and mobile card views - Add spacious flat table CSS for desktop (transparent card wrapper, lighter dividers, visible hover, larger cell padding) - Split roaster/cafe columns into separate Country and City columns with sortable headers - Split cup Roast/Roaster into separate desktop columns with city sort support (full-stack: domain, repo, view, template) - Add sortable Status column to bags with colored pills (green Open, amber Closed) and remaining weight subtext - Style bag Finished column as date-only (NaiveDate) and Weight column with smaller text - Show status pill next to mobile bag card actions menu - Add city to cups mobile card view - Use middle dot separator for brew recipe weight/volume - Add tasting notes pills to roast list, brew notes to brew list - Fix build.rs to use rerun-if-changed for CSS and templates --- build.rs | 11 +++++ src/domain/bags.rs | 5 ++- src/domain/cups.rs | 7 ++++ src/infrastructure/repositories/bags.rs | 1 + src/infrastructure/repositories/cups.rs | 11 ++++- src/presentation/web/views/bags.rs | 6 +-- src/presentation/web/views/cups.rs | 2 + static/css/input.css | 30 ++++++++++++++ templates/partials/lists/bag_list.html | 48 ++++++++++++++-------- templates/partials/lists/brew_list.html | 21 ++++++---- templates/partials/lists/cafe_list.html | 16 ++++---- templates/partials/lists/cup_list.html | 17 +++++--- templates/partials/lists/gear_list.html | 7 ++-- templates/partials/lists/roast_list.html | 16 +++++--- templates/partials/lists/roaster_list.html | 18 ++++---- 15 files changed, 153 insertions(+), 63 deletions(-) diff --git a/build.rs b/build.rs index bb45018..1bd3a21 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::process::Command; fn main() { @@ -18,6 +19,9 @@ fn git_hash() { } fn tailwind() { + println!("cargo:rerun-if-changed=static/css/input.css"); + println!("cargo:rerun-if-changed=templates/"); + let mut cmd = Command::new("tailwindcss"); cmd.args(["-i", "static/css/input.css", "-o", "static/css/styles.css"]); @@ -25,6 +29,8 @@ fn tailwind() { cmd.arg("--minify"); } + let output = Path::new("static/css/styles.css"); + match cmd.status() { Ok(status) if status.success() => {} Ok(status) => { @@ -37,4 +43,9 @@ fn tailwind() { eprintln!("cargo:warning=failed to run tailwindcss: {e}"); } } + + // Ensure the file exists so include_str!() doesn't break the build + if !output.exists() { + std::fs::write(output, "/* tailwindcss not available */").ok(); + } } diff --git a/src/domain/bags.rs b/src/domain/bags.rs index 49c4eee..f35457f 100644 --- a/src/domain/bags.rs +++ b/src/domain/bags.rs @@ -89,6 +89,7 @@ pub enum BagSortKey { UpdatedAt, Roaster, Roast, + Status, FinishedAt, } @@ -104,6 +105,7 @@ impl SortKey for BagSortKey { "updated-at" => Some(BagSortKey::UpdatedAt), "roaster" => Some(BagSortKey::Roaster), "roast" => Some(BagSortKey::Roast), + "status" => Some(BagSortKey::Status), "finished-at" => Some(BagSortKey::FinishedAt), _ => None, } @@ -116,13 +118,14 @@ impl SortKey for BagSortKey { BagSortKey::UpdatedAt => "updated-at", BagSortKey::Roaster => "roaster", BagSortKey::Roast => "roast", + BagSortKey::Status => "status", BagSortKey::FinishedAt => "finished-at", } } fn default_direction(self) -> SortDirection { match self { - BagSortKey::Roaster | BagSortKey::Roast => SortDirection::Asc, + BagSortKey::Roaster | BagSortKey::Roast | BagSortKey::Status => SortDirection::Asc, _ => SortDirection::Desc, } } diff --git a/src/domain/cups.rs b/src/domain/cups.rs index 51f598a..eee27af 100644 --- a/src/domain/cups.rs +++ b/src/domain/cups.rs @@ -24,6 +24,7 @@ pub struct CupWithDetails { pub roaster_slug: String, pub cafe_name: String, pub cafe_slug: String, + pub cafe_city: String, } impl CupWithDetails { @@ -96,7 +97,9 @@ impl CupFilter { pub enum CupSortKey { CreatedAt, CafeName, + CafeCity, RoastName, + RoasterName, } impl SortKey for CupSortKey { @@ -108,7 +111,9 @@ impl SortKey for CupSortKey { match value { "created-at" => Some(CupSortKey::CreatedAt), "cafe" => Some(CupSortKey::CafeName), + "city" => Some(CupSortKey::CafeCity), "roast" => Some(CupSortKey::RoastName), + "roaster" => Some(CupSortKey::RoasterName), _ => None, } } @@ -117,7 +122,9 @@ impl SortKey for CupSortKey { match self { CupSortKey::CreatedAt => "created-at", CupSortKey::CafeName => "cafe", + CupSortKey::CafeCity => "city", CupSortKey::RoastName => "roast", + CupSortKey::RoasterName => "roaster", } } diff --git a/src/infrastructure/repositories/bags.rs b/src/infrastructure/repositories/bags.rs index 05a8130..91f5778 100644 --- a/src/infrastructure/repositories/bags.rs +++ b/src/infrastructure/repositories/bags.rs @@ -42,6 +42,7 @@ impl SqlBagRepository { BagSortKey::UpdatedAt => format!("b.updated_at {dir_sql}, b.id DESC"), BagSortKey::Roaster => format!("LOWER(rr.name) {dir_sql}, b.created_at DESC"), BagSortKey::Roast => format!("LOWER(r.name) {dir_sql}, b.created_at DESC"), + BagSortKey::Status => format!("b.closed {dir_sql}, b.created_at DESC"), BagSortKey::FinishedAt => format!("b.finished_at {dir_sql}, b.created_at DESC"), } } diff --git a/src/infrastructure/repositories/cups.rs b/src/infrastructure/repositories/cups.rs index 1591c41..d6695ca 100644 --- a/src/infrastructure/repositories/cups.rs +++ b/src/infrastructure/repositories/cups.rs @@ -15,7 +15,8 @@ const BASE_SELECT: &str = r" c.created_at, c.updated_at, r.name as roast_name, r.slug as roast_slug, rr.name as roaster_name, rr.slug as roaster_slug, - ca.name as cafe_name, ca.slug as cafe_slug + ca.name as cafe_name, ca.slug as cafe_slug, + ca.city as cafe_city FROM cups c JOIN roasts r ON c.roast_id = r.id JOIN roasters rr ON r.roaster_id = rr.id @@ -43,9 +44,15 @@ impl SqlCupRepository { CupSortKey::CafeName => { format!("LOWER(ca.name) {dir_sql}, c.created_at DESC") } + CupSortKey::CafeCity => { + format!("LOWER(ca.city) {dir_sql}, c.created_at DESC") + } CupSortKey::RoastName => { format!("LOWER(r.name) {dir_sql}, c.created_at DESC") } + CupSortKey::RoasterName => { + format!("LOWER(rr.name) {dir_sql}, c.created_at DESC") + } } } @@ -74,6 +81,7 @@ impl SqlCupRepository { roaster_slug: record.roaster_slug, cafe_name: record.cafe_name, cafe_slug: record.cafe_slug, + cafe_city: record.cafe_city, } } @@ -219,4 +227,5 @@ struct CupWithDetailsRecord { roaster_slug: String, cafe_name: String, cafe_slug: String, + cafe_city: String, } diff --git a/src/presentation/web/views/bags.rs b/src/presentation/web/views/bags.rs index 9d10c16..1b9329a 100644 --- a/src/presentation/web/views/bags.rs +++ b/src/presentation/web/views/bags.rs @@ -8,7 +8,7 @@ pub struct BagView { pub amount: String, pub remaining: String, pub closed: bool, - pub finished_at: String, + pub finished_date: String, pub created_date: String, pub created_time: String, pub roast_name: String, @@ -26,10 +26,10 @@ impl BagView { amount: format!("{:.1}", bag.bag.amount), remaining: format!("{:.1}", bag.bag.remaining), closed: bag.bag.closed, - finished_at: bag + finished_date: bag .bag .finished_at - .map_or_else(|| "—".to_string(), |d| 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_time: bag.bag.created_at.format("%H:%M").to_string(), roast_name: bag.roast_name, diff --git a/src/presentation/web/views/cups.rs b/src/presentation/web/views/cups.rs index 4ed977a..70cd36e 100644 --- a/src/presentation/web/views/cups.rs +++ b/src/presentation/web/views/cups.rs @@ -9,6 +9,7 @@ pub struct CupView { pub roaster_slug: String, pub cafe_name: String, pub cafe_slug: String, + pub cafe_city: String, pub created_date: String, pub created_time: String, } @@ -23,6 +24,7 @@ impl CupView { roaster_slug: cup.roaster_slug, cafe_name: cup.cafe_name, cafe_slug: cup.cafe_slug, + cafe_city: cup.cafe_city, created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(), created_time: cup.cup.created_at.format("%H:%M").to_string(), } diff --git a/static/css/input.css b/static/css/input.css index 2e6e56c..28dbfc0 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -548,14 +548,44 @@ a { border-bottom-width: 1px; } + /* Hide Actions column on desktop — whole row is clickable */ + .responsive-table .actions-col, + .responsive-table td.card-actions { + display: none; + } + + /* Pointer cursor on clickable rows */ + .responsive-table tbody > tr[onclick] { + cursor: pointer; + } + /* Hover: accent tint + left bar */ .responsive-table tbody > tr:not(.detail-row):hover { background-color: var(--accent-subtle); box-shadow: inset 3px 0 0 var(--accent); } + /* Expanded row: persistent accent highlight */ + .responsive-table tbody > tr.expanded { + background-color: var(--accent-subtle); + box-shadow: inset 3px 0 0 var(--accent); + } + + /* Expanded row: remove bottom border so detail row merges */ + .responsive-table tbody > tr.expanded { + border-bottom-width: 0; + } + + /* Detail row: light grey bg + left accent bar, no top border */ + .responsive-table tbody > tr.expanded + tr.detail-row { + background-color: var(--surface-alt); + box-shadow: inset 3px 0 0 var(--accent); + border-top-width: 0; + } + /* Detail row: keep compact */ .responsive-table .detail-row td { + background-color: transparent; padding-top: 0.5rem; padding-bottom: 0.5rem; } diff --git a/templates/partials/lists/bag_list.html b/templates/partials/lists/bag_list.html index 6c81544..8c6c7ed 100644 --- a/templates/partials/lists/bag_list.html +++ b/templates/partials/lists/bag_list.html @@ -27,16 +27,18 @@ {% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %} {% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %} Weight - Status + {% call table::sortable_header("Status", "status", navigator, "#bag-list") %} {% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %} {% if is_authenticated %} - Actions + Actions {% endif %} {% for bag in bags.items %} - +
{{ bag.created_date }}
@@ -44,20 +46,24 @@ {{ bag.roaster_name }} - {{ bag.roast_name }} - {{ bag.amount }}g - + + {{ bag.roast_name }} + + {{ bag.amount }}g + {% if bag.closed %} - Closed + Closed {% else %} - Open - + Open +
{{ bag.remaining }}g left
{% endif %} {% if !bag.closed %} {{ bag.remaining }}g left {% endif %} - {{ bag.finished_at }} + +
{{ bag.finished_date }}
+ {% if is_authenticated %}
@@ -76,13 +82,21 @@
- +
+ + {% if bag.closed %} + Closed + {% else %} + Open + {% endif %} + + +
{% endif %} diff --git a/templates/partials/lists/brew_list.html b/templates/partials/lists/brew_list.html index 73e8608..8c3dc06 100644 --- a/templates/partials/lists/brew_list.html +++ b/templates/partials/lists/brew_list.html @@ -58,13 +58,15 @@ Brewer Notes {% if is_authenticated %} - Actions + Actions {% endif %} {% for brew in brews.items %} - +
{{ brew.created_date }}
@@ -80,7 +82,7 @@ {{ brew.grinder_name }} -
{{ brew.coffee_weight }} / {{ brew.water_volume }}
+
{{ brew.coffee_weight }} · {{ brew.water_volume }}
{{ brew.ratio }} @@ -94,11 +96,15 @@ {% if let Some(fp_name) = brew.filter_paper_name %} {{ fp_name }} {% endif %} - - {% if !brew.quick_notes_label.is_empty() %} - {{ brew.quick_notes_label }} + + {% if !brew.quick_notes.is_empty() %} +
+ {% for qn in brew.quick_notes %} + {{ qn.label }} + {% endfor %} +
{% else %} - + No Notes {% endif %} {% if is_authenticated %} @@ -108,7 +114,6 @@