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
This commit is contained in:
parent
c2cbbd58d7
commit
3a960b9d14
15 changed files with 153 additions and 63 deletions
11
build.rs
11
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,18 @@
|
|||
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %}
|
||||
{% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %}
|
||||
<th scope="col" class="px-4 py-3">Weight</th>
|
||||
<th scope="col" class="px-4 py-3">Status</th>
|
||||
{% call table::sortable_header("Status", "status", navigator, "#bag-list") %}
|
||||
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for bag in bags.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ bag.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ bag.created_time }}</div>
|
||||
|
|
@ -44,20 +46,24 @@
|
|||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-text-secondary">
|
||||
{{ bag.roaster_name }}
|
||||
</td>
|
||||
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">{{ bag.roast_name }}</td>
|
||||
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap">{{ bag.amount }}g</td>
|
||||
<td data-label="Status" class="px-4 py-3 whitespace-nowrap">
|
||||
<td data-label="Roast" class="card-title px-4 py-3 whitespace-nowrap">
|
||||
{{ bag.roast_name }}
|
||||
</td>
|
||||
<td data-label="Weight" class="px-4 py-3 whitespace-nowrap text-xs font-medium text-text-secondary">{{ bag.amount }}g</td>
|
||||
<td data-label="Status" class="mobile-hidden px-4 py-3 whitespace-nowrap md:text-center">
|
||||
{% if bag.closed %}
|
||||
Closed
|
||||
<span class="pill pill-warning">Closed</span>
|
||||
{% else %}
|
||||
<span class="font-medium text-emerald-600">Open</span>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ bag.remaining }}g left</div>
|
||||
<span class="pill pill-success">Open</span>
|
||||
<div class="text-xs text-text-muted">{{ bag.remaining }}g left</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if !bag.closed %}
|
||||
<td data-label="Remaining" class="px-4 py-3 whitespace-nowrap md:hidden">{{ bag.remaining }}g left</td>
|
||||
{% endif %}
|
||||
<td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
|
||||
<td data-label="Finished" class="whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ bag.finished_date }}</div>
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -76,13 +82,21 @@
|
|||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<span class="md:hidden">
|
||||
{% if bag.closed %}
|
||||
<span class="pill pill-warning text-[0.625rem] py-0 px-1.5">Closed</span>
|
||||
{% else %}
|
||||
<span class="pill pill-success text-[0.625rem] py-0 px-1.5">Open</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -58,13 +58,15 @@
|
|||
<th scope="col" class="px-4 py-3">Brewer</th>
|
||||
<th scope="col" class="px-4 py-3">Notes</th>
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for brew in brews.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ brew.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ brew.created_time }}</div>
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
</td>
|
||||
<td data-label="Grinder" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.grinder_name }}</td>
|
||||
<td data-label="Recipe" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.coffee_weight }} / {{ brew.water_volume }}</div>
|
||||
<div>{{ brew.coffee_weight }} · {{ brew.water_volume }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.ratio }} · {{ brew.water_temp }}</div>
|
||||
</td>
|
||||
<td data-label="Ratio" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.ratio }}</td>
|
||||
|
|
@ -94,11 +96,15 @@
|
|||
{% if let Some(fp_name) = brew.filter_paper_name %}
|
||||
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Notes" class="px-4 py-3 text-xs text-text-muted">
|
||||
{% if !brew.quick_notes_label.is_empty() %}
|
||||
{{ brew.quick_notes_label }}
|
||||
<td data-label="Notes" class="px-4 py-3">
|
||||
{% if !brew.quick_notes.is_empty() %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for qn in brew.quick_notes %}
|
||||
<span class="{{ qn.pill_class }}">{{ qn.label }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-text-muted">—</span>
|
||||
<span class="pill pill-muted">No Notes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -108,7 +114,6 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@
|
|||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#cafe-list") %}
|
||||
{% call table::sortable_header("Name", "name", navigator, "#cafe-list") %}
|
||||
{% call table::sortable_header("Location", "country", navigator, "#cafe-list") %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
{% call table::sortable_header("City", "city", navigator, "#cafe-list") %}
|
||||
{% call table::sortable_header("Country", "country", navigator, "#cafe-list") %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
data-sort-country="{{ cafe.country }}"
|
||||
data-sort-city="{{ cafe.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ cafe.created_date }}</div>
|
||||
|
|
@ -46,11 +48,10 @@
|
|||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
|
||||
{{ cafe.name }}
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ cafe.country }}
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ cafe.city }}</div>
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.country }}</td>
|
||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.city }}</td>
|
||||
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.country }}</td>
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
|
|
@ -75,7 +76,6 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="4" class="bg-surface-alt px-4 py-2">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@
|
|||
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
|
||||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("Roast", "roast", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("Roaster", "roaster", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
|
||||
{% call table::sortable_header("City", "city", navigator, "#cup-list") %}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -36,19 +38,23 @@
|
|||
<tr
|
||||
data-star-key="{{ cup.id }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ cup.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ cup.created_time }}</div>
|
||||
</td>
|
||||
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap">
|
||||
<td data-label="Coffee" class="card-title px-4 py-3 whitespace-nowrap md:hidden">
|
||||
<div class="font-medium text-text">{{ cup.roast_name }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ cup.roaster_name }}</div>
|
||||
</td>
|
||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.roaster_name }}</td>
|
||||
<td data-label="Roast" class="mobile-hidden px-4 py-3 whitespace-nowrap font-medium text-text">{{ cup.roast_name }}</td>
|
||||
<td data-label="Roaster" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cup.roaster_name }}</td>
|
||||
<td data-label="Cafe" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ cup.cafe_name }}
|
||||
</td>
|
||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.cafe_city }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cup.cafe_city }}</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -62,7 +68,6 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
@ -72,7 +77,7 @@
|
|||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="4" class="bg-surface-alt px-4 py-2">
|
||||
<td colspan="6" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
|
|
|
|||
|
|
@ -29,13 +29,15 @@
|
|||
{% call table::sortable_header("Make", "make", navigator, "#gear-list") %}
|
||||
{% call table::sortable_header("Model", "model", navigator, "#gear-list") %}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for item in gear.items %}
|
||||
<tr class="transition hover:bg-surface-alt">
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ item.created_date }}</div>
|
||||
<div class="hidden md:block font-normal text-text-muted">{{ item.created_time }}</div>
|
||||
|
|
@ -63,7 +65,6 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
{% call table::sortable_header("Origin", "origin", navigator, "#roast-list") %}
|
||||
<th scope="col" class="px-4 py-3">Tasting Notes</th>
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
data-sort-origin="{{ roast.origin }}"
|
||||
data-sort-producer="{{ roast.producer }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ roast.created_date }}</div>
|
||||
|
|
@ -61,11 +62,15 @@
|
|||
{% if !roast.producer.is_empty() %}
|
||||
<td data-label="Producer" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.producer }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Tasting Notes" class="px-4 py-3 text-xs text-text-muted text-right md:text-left">
|
||||
{% if roast.tasting_notes.is_empty() %}
|
||||
<span class="text-text-muted">—</span>
|
||||
<td data-label="Tasting Notes" class="px-4 py-3">
|
||||
{% if !roast.tasting_notes.is_empty() %}
|
||||
<div class="flex flex-wrap gap-1 md:justify-start justify-end">
|
||||
{% for note in roast.tasting_notes %}
|
||||
<span class="pill pill-muted">{{ note }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ roast.tasting_notes.join(", ") }}
|
||||
<span class="pill pill-muted">No Notes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
|
|
@ -81,7 +86,6 @@
|
|||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@
|
|||
<tr>
|
||||
{% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %}
|
||||
{% call table::sortable_header("Name", "name", navigator, "#roaster-list") %}
|
||||
{% call table::sortable_header("Location", "country", navigator, "#roaster-list") %}
|
||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||
{% call table::sortable_header("Country", "country", navigator, "#roaster-list") %}
|
||||
{% call table::sortable_header("City", "city", navigator, "#roaster-list") %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
data-sort-country="{{ roaster.country }}"
|
||||
data-sort-city="{{ roaster.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if roaster.has_homepage || is_authenticated %}onclick="if(event.target.closest('.card-detail'))return;this.classList.toggle('expanded');this.querySelector('.card-detail')?.classList.toggle('hidden');const r=this.nextElementSibling;if(r?.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand')?.classList.toggle('hidden');this.querySelector('.icon-collapse')?.classList.toggle('hidden');"{% endif %}
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-text-secondary">
|
||||
<div>{{ roaster.created_date }}</div>
|
||||
|
|
@ -46,15 +48,12 @@
|
|||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-text">
|
||||
{{ roaster.name }}
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap">
|
||||
{{ roaster.country }}
|
||||
{% if !roaster.city.is_empty() %}
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ roaster.city }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Location" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.country }}</td>
|
||||
{% if !roaster.city.is_empty() %}
|
||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.city }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.city }}</td>
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -78,7 +77,6 @@
|
|||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
onclick="const tr=this.closest('tr');const d=tr.querySelector('.card-detail');if(d)d.classList.toggle('hidden');const r=tr.nextElementSibling;if(r&&r.classList.contains('detail-row'))r.classList.toggle('hidden');this.querySelector('.icon-expand').classList.toggle('hidden');this.querySelector('.icon-collapse').classList.toggle('hidden');"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||
|
|
@ -88,7 +86,7 @@
|
|||
</tr>
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="4" class="bg-surface-alt px-4 py-2">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
{% if roaster.has_homepage %}
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
|
|
|
|||
Loading…
Reference in a new issue