refactor(ui): improve data table detail rows and mobile cards
- Split created_at into created_date + created_time across all view models - Add inline card-detail pattern for mobile action buttons - Add desktop detail-row with ellipsis toggle for table actions - Add pill-warning and pill-success CSS variants with dark mode - Center mobile card-detail buttons, fix card-date vertical spacing - Rename action labels: "View on Map" → "View Map", "Visit website/homepage" → "Homepage"
This commit is contained in:
parent
35ea246ba2
commit
3bc6fae656
15 changed files with 351 additions and 315 deletions
|
|
@ -9,7 +9,8 @@ pub struct BagView {
|
||||||
pub remaining: String,
|
pub remaining: String,
|
||||||
pub closed: bool,
|
pub closed: bool,
|
||||||
pub finished_at: String,
|
pub finished_at: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
pub roast_name: String,
|
pub roast_name: String,
|
||||||
pub roaster_name: String,
|
pub roaster_name: String,
|
||||||
pub roast_slug: String,
|
pub roast_slug: String,
|
||||||
|
|
@ -29,7 +30,8 @@ impl BagView {
|
||||||
.bag
|
.bag
|
||||||
.finished_at
|
.finished_at
|
||||||
.map_or_else(|| "—".to_string(), |d| d.to_string()),
|
.map_or_else(|| "—".to_string(), |d| d.to_string()),
|
||||||
created_at: bag.bag.created_at.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,
|
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,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ pub struct BrewView {
|
||||||
pub water_volume: String,
|
pub water_volume: String,
|
||||||
pub water_temp: String,
|
pub water_temp: String,
|
||||||
pub ratio: String,
|
pub ratio: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
pub relative_date_label: String,
|
pub relative_date_label: String,
|
||||||
// Raw values for "brew again" feature
|
// Raw values for "brew again" feature
|
||||||
pub coffee_weight_raw: f64,
|
pub coffee_weight_raw: f64,
|
||||||
|
|
@ -62,7 +63,8 @@ impl BrewView {
|
||||||
water_volume: format!("{}ml", brew.brew.water_volume),
|
water_volume: format!("{}ml", brew.brew.water_volume),
|
||||||
water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp),
|
water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp),
|
||||||
ratio,
|
ratio,
|
||||||
created_at: brew.brew.created_at.format("%Y-%m-%d %H:%M").to_string(),
|
created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(),
|
||||||
|
created_time: brew.brew.created_at.format("%H:%M").to_string(),
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ pub struct CafeView {
|
||||||
pub has_website: bool,
|
pub has_website: bool,
|
||||||
pub website_url: String,
|
pub website_url: String,
|
||||||
pub website_label: String,
|
pub website_label: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
pub created_at_sort_key: i64,
|
pub created_at_sort_key: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +39,8 @@ impl From<Cafe> for CafeView {
|
||||||
let map_url = format!("https://www.google.com/maps?q={latitude},{longitude}");
|
let map_url = format!("https://www.google.com/maps?q={latitude},{longitude}");
|
||||||
|
|
||||||
let created_at_sort_key = created_at.timestamp();
|
let created_at_sort_key = created_at.timestamp();
|
||||||
let created_at_label = created_at.format("%Y-%m-%d").to_string();
|
let created_date = created_at.format("%Y-%m-%d").to_string();
|
||||||
|
let created_time = created_at.format("%H:%M").to_string();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
detail_path,
|
detail_path,
|
||||||
|
|
@ -52,7 +54,8 @@ impl From<Cafe> for CafeView {
|
||||||
has_website,
|
has_website,
|
||||||
website_url: website.clone(),
|
website_url: website.clone(),
|
||||||
website_label: website,
|
website_label: website,
|
||||||
created_at: created_at_label,
|
created_date,
|
||||||
|
created_time,
|
||||||
created_at_sort_key,
|
created_at_sort_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ pub struct CupView {
|
||||||
pub roaster_slug: String,
|
pub roaster_slug: String,
|
||||||
pub cafe_name: String,
|
pub cafe_name: String,
|
||||||
pub cafe_slug: String,
|
pub cafe_slug: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CupView {
|
impl CupView {
|
||||||
|
|
@ -22,7 +23,8 @@ impl CupView {
|
||||||
roaster_slug: cup.roaster_slug,
|
roaster_slug: cup.roaster_slug,
|
||||||
cafe_name: cup.cafe_name,
|
cafe_name: cup.cafe_name,
|
||||||
cafe_slug: cup.cafe_slug,
|
cafe_slug: cup.cafe_slug,
|
||||||
created_at: cup.cup.created_at.format("%Y-%m-%d %H:%M").to_string(),
|
created_date: cup.cup.created_at.format("%Y-%m-%d").to_string(),
|
||||||
|
created_time: cup.cup.created_at.format("%H:%M").to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ pub struct GearView {
|
||||||
pub make: String,
|
pub make: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub full_name: String,
|
pub full_name: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GearView {
|
impl GearView {
|
||||||
|
|
@ -20,7 +21,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_at: gear.created_at.format("%Y-%m-%d").to_string(),
|
created_date: gear.created_at.format("%Y-%m-%d").to_string(),
|
||||||
|
created_time: gear.created_at.format("%H:%M").to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ pub struct RoasterView {
|
||||||
pub has_homepage: bool,
|
pub has_homepage: bool,
|
||||||
pub homepage_url: String,
|
pub homepage_url: String,
|
||||||
pub homepage_label: String,
|
pub homepage_label: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
pub created_at_sort_key: i64,
|
pub created_at_sort_key: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +54,8 @@ impl From<Roaster> for RoasterView {
|
||||||
let detail_path = format!("/roasters/{slug}");
|
let detail_path = format!("/roasters/{slug}");
|
||||||
|
|
||||||
let created_at_sort_key = created_at.timestamp();
|
let created_at_sort_key = created_at.timestamp();
|
||||||
let created_at_label = created_at.format("%Y-%m-%d").to_string();
|
let created_date = created_at.format("%Y-%m-%d").to_string();
|
||||||
|
let created_time = created_at.format("%H:%M").to_string();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
detail_path,
|
detail_path,
|
||||||
|
|
@ -64,7 +66,8 @@ impl From<Roaster> for RoasterView {
|
||||||
has_homepage,
|
has_homepage,
|
||||||
homepage_url: homepage.clone(),
|
homepage_url: homepage.clone(),
|
||||||
homepage_label: homepage,
|
homepage_label: homepage,
|
||||||
created_at: created_at_label,
|
created_date,
|
||||||
|
created_time,
|
||||||
created_at_sort_key,
|
created_at_sort_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ pub struct RoastView {
|
||||||
pub region: String,
|
pub region: String,
|
||||||
pub producer: String,
|
pub producer: String,
|
||||||
pub process: String,
|
pub process: String,
|
||||||
pub created_at: String,
|
pub created_date: String,
|
||||||
|
pub created_time: String,
|
||||||
pub created_at_sort_key: i64,
|
pub created_at_sort_key: i64,
|
||||||
pub tasting_notes: Vec<String>,
|
pub tasting_notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +65,8 @@ impl RoastView {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let created_at = created_at.format("%Y-%m-%d").to_string();
|
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 {
|
||||||
|
|
@ -77,7 +79,8 @@ impl RoastView {
|
||||||
region,
|
region,
|
||||||
producer,
|
producer,
|
||||||
process,
|
process,
|
||||||
created_at,
|
created_date,
|
||||||
|
created_time,
|
||||||
created_at_sort_key,
|
created_at_sort_key,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,28 @@ a {
|
||||||
background-color: var(--surface-alt);
|
background-color: var(--surface-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pill-warning {
|
||||||
|
border-color: rgba(217, 119, 6, 0.4);
|
||||||
|
color: #92400e;
|
||||||
|
background-color: #fffbeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pill-warning {
|
||||||
|
color: #fbbf24;
|
||||||
|
background-color: rgba(217, 119, 6, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-success {
|
||||||
|
border-color: rgba(5, 150, 105, 0.4);
|
||||||
|
color: #065f46;
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pill-success {
|
||||||
|
color: #34d399;
|
||||||
|
background-color: rgba(5, 150, 105, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Tabs ──────────────────────────────────────────────────────── */
|
/* ── Tabs ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
|
|
@ -367,8 +389,8 @@ a {
|
||||||
/* "Added" date: footer section with top divider */
|
/* "Added" date: footer section with top divider */
|
||||||
.responsive-table td.card-date {
|
.responsive-table td.card-date {
|
||||||
order: 99;
|
order: 99;
|
||||||
padding-top: 0.5rem;
|
margin: 0.5rem -1rem -1rem !important;
|
||||||
margin-top: 0.5rem;
|
padding: 0.75rem 1rem !important;
|
||||||
border-top: 1px solid var(--border-muted) !important;
|
border-top: 1px solid var(--border-muted) !important;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
@ -396,84 +418,29 @@ a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Three-dot dropdown menu */
|
/* Hide detail row on mobile — inline card-detail is used instead */
|
||||||
.card-menu {
|
.responsive-table .detail-row {
|
||||||
position: relative;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-menu summary {
|
/* Inline detail content within cards */
|
||||||
list-style: none;
|
.responsive-table .card-detail {
|
||||||
cursor: pointer;
|
order: -1;
|
||||||
display: inline-flex;
|
margin: 0 -1rem 0.25rem !important;
|
||||||
height: 2rem;
|
padding: 0.5rem 1rem !important;
|
||||||
width: 2rem;
|
border-bottom: 1px solid var(--border-muted) !important;
|
||||||
align-items: center;
|
background-color: var(--surface-alt);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: color 150ms ease, background-color 150ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-menu summary:hover {
|
.responsive-table .card-detail.hidden {
|
||||||
color: var(--accent);
|
display: none !important;
|
||||||
background-color: var(--surface-alt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-menu summary::-webkit-details-marker {
|
.responsive-table .card-detail::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-menu[open] summary {
|
|
||||||
color: var(--accent);
|
|
||||||
background-color: var(--surface-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-menu .card-menu-items {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
z-index: 20;
|
|
||||||
min-width: 10rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background-color: var(--surface);
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-menu .card-menu-items a,
|
|
||||||
.card-menu .card-menu-items button,
|
|
||||||
.card-menu .card-menu-items form button {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text);
|
|
||||||
text-align: left;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 150ms ease;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-menu .card-menu-items a:hover,
|
|
||||||
.card-menu .card-menu-items button:hover,
|
|
||||||
.card-menu .card-menu-items form button:hover {
|
|
||||||
background-color: var(--surface-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-menu .card-menu-items .menu-item-danger {
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-menu .card-menu-items .menu-item-danger:hover {
|
|
||||||
background-color: rgba(254, 202, 202, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-controls {
|
.pagination-controls {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -482,6 +449,11 @@ a {
|
||||||
/* ── Desktop timeline ──────────────────────────────────────────── */
|
/* ── Desktop timeline ──────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
/* Hide inline card-detail on desktop — detail row is used instead */
|
||||||
|
.responsive-table .card-detail {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-list {
|
.timeline-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
{% for bag in bags.items %}
|
{% for bag in bags.items %}
|
||||||
<tr class="transition hover:bg-surface-alt">
|
<tr class="transition hover:bg-surface-alt">
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ bag.created_at }}
|
<div>{{ bag.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ bag.created_time }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-stone-600">
|
<td data-label="Roaster" class="px-4 py-3 whitespace-nowrap font-medium text-stone-600">
|
||||||
{{ bag.roaster_name }}
|
{{ bag.roaster_name }}
|
||||||
|
|
@ -58,47 +59,53 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
|
<td data-label="Finished" class="px-4 py-3 whitespace-nowrap">{{ bag.finished_at }}</td>
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
<td data-label="" class="card-detail hidden">
|
||||||
<div class="hidden md:inline-flex items-center gap-1">
|
<div class="flex items-center gap-4">
|
||||||
{% if !bag.closed %}
|
{% if !bag.closed %}
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||||
title="Close bag"
|
{% call icons::x_circle("h-4 w-4") %} Close bag
|
||||||
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Close bag</span>
|
|
||||||
{% call icons::x_circle("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||||
title="Delete bag"
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Delete</span>
|
|
||||||
{% call icons::delete("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<details class="card-menu md:hidden">
|
</td>
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<div class="card-menu-items">
|
<button type="button"
|
||||||
{% if !bag.closed %}
|
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||||
<button type="button"
|
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');"
|
||||||
data-on:click="this.closest('details').open = false; confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
title="Actions">
|
||||||
{% call icons::x_circle("h-4 w-4") %} Close bag
|
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||||
</button>
|
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||||
{% endif %}
|
</button>
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="7" class="bg-surface-alt px-4 py-2">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
{% if !bag.closed %}
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
|
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||||
|
{% call icons::x_circle("h-4 w-4") %} Close bag
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
{% for brew in brews.items %}
|
{% for brew in brews.items %}
|
||||||
<tr class="transition hover:bg-surface-alt">
|
<tr class="transition hover:bg-surface-alt">
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ brew.created_at }}
|
<div>{{ brew.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ brew.created_time }}</div>
|
||||||
</td>
|
</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">
|
||||||
<div class="font-medium text-stone-800">{{ brew.roast_name }}</div>
|
<div class="font-medium text-stone-800">{{ brew.roast_name }}</div>
|
||||||
|
|
@ -66,8 +67,8 @@
|
||||||
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
<td data-label="Filter" class="px-4 py-3 whitespace-nowrap md:hidden">{{ fp_name }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
<td data-label="" class="card-detail hidden">
|
||||||
<div class="hidden md:inline-flex items-center gap-1">
|
<div class="flex items-center gap-4">
|
||||||
<form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
<form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||||
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
||||||
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
||||||
|
|
@ -79,52 +80,58 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||||
<button
|
<button type="submit"
|
||||||
type="submit"
|
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
{% call icons::plus_circle("h-4 w-4") %} Brew Again
|
||||||
title="Brew again"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Brew again</span>
|
|
||||||
{% call icons::refresh("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||||
title="Delete brew"
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Delete</span>
|
|
||||||
{% call icons::delete("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<details class="card-menu md:hidden">
|
</td>
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<div class="card-menu-items">
|
<button type="button"
|
||||||
<form class="inline" data-on:submit="this.closest('details').open = false; @post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||||
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
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');"
|
||||||
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
title="Actions">
|
||||||
<input type="hidden" name="grinder_id" value="{{ brew.grinder_id }}" />
|
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||||
<input type="hidden" name="grind_setting" value="{{ brew.grind_setting_raw }}" />
|
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||||
<input type="hidden" name="brewer_id" value="{{ brew.brewer_id }}" />
|
</button>
|
||||||
{% if let Some(fp_id) = brew.filter_paper_id %}
|
|
||||||
<input type="hidden" name="filter_paper_id" value="{{ fp_id }}" />
|
|
||||||
{% endif %}
|
|
||||||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
|
||||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
|
||||||
<button type="submit">
|
|
||||||
{% call icons::refresh("h-4 w-4") %} Brew again
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="7" class="bg-surface-alt px-4 py-2">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<form class="inline" data-on:submit="@post('/api/v1/brews?{{ navigator.query() }}', {contentType: 'form', responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||||
|
<input type="hidden" name="bag_id" value="{{ brew.bag_id }}" />
|
||||||
|
<input type="hidden" name="coffee_weight" value="{{ brew.coffee_weight_raw }}" />
|
||||||
|
<input type="hidden" name="grinder_id" value="{{ brew.grinder_id }}" />
|
||||||
|
<input type="hidden" name="grind_setting" value="{{ brew.grind_setting_raw }}" />
|
||||||
|
<input type="hidden" name="brewer_id" value="{{ brew.brewer_id }}" />
|
||||||
|
{% if let Some(fp_id) = brew.filter_paper_id %}
|
||||||
|
<input type="hidden" name="filter_paper_id" value="{{ fp_id }}" />
|
||||||
|
{% endif %}
|
||||||
|
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||||
|
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
|
||||||
|
{% call icons::plus_circle("h-4 w-4") %} Brew Again
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@
|
||||||
class="transition hover:bg-surface-alt"
|
class="transition hover:bg-surface-alt"
|
||||||
>
|
>
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ cafe.created_at }}
|
<div>{{ cafe.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ cafe.created_time }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
||||||
{{ cafe.name }}
|
{{ cafe.name }}
|
||||||
|
|
@ -50,69 +51,58 @@
|
||||||
<div class="hidden md:block text-xs text-stone-500">{{ cafe.city }}</div>
|
<div class="hidden md:block text-xs text-stone-500">{{ cafe.city }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
|
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
<td data-label="" class="card-detail hidden">
|
||||||
<div class="hidden md:inline-flex items-center gap-1">
|
<div class="flex items-center gap-4">
|
||||||
<a
|
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
|
||||||
href="{{ cafe.map_url }}"
|
{% call icons::location("h-4 w-4") %} View Map
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
title="Open in Google Maps"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Open {{ cafe.name }} in Google Maps</span>
|
|
||||||
{% call icons::location("h-4 w-4") %}
|
|
||||||
</a>
|
</a>
|
||||||
{% if cafe.has_website %}
|
{% if cafe.has_website %}
|
||||||
<a
|
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
|
||||||
href="{{ cafe.website_url }}"
|
{% call icons::external_link("h-4 w-4") %} Homepage
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
title="Visit website"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Visit {{ cafe.name }} website</span>
|
|
||||||
{% call icons::external_link("h-4 w-4") %}
|
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
|
||||||
<span
|
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-300 cursor-default"
|
|
||||||
title="No website"
|
|
||||||
aria-disabled="true"
|
|
||||||
>
|
|
||||||
{% call icons::external_link("h-4 w-4") %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||||
title="Delete cafe"
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})"
|
</button>
|
||||||
>
|
{% endif %}
|
||||||
<span class="sr-only">Delete</span>
|
</div>
|
||||||
{% call icons::delete("h-4 w-4") %}
|
</td>
|
||||||
|
<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-stone-400 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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="4" 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">
|
||||||
|
{% call icons::location("h-4 w-4") %} View Map
|
||||||
|
</a>
|
||||||
|
{% if cafe.has_website %}
|
||||||
|
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
|
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
|
||||||
|
{% call icons::external_link("h-4 w-4") %} Homepage
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<details class="card-menu md:hidden">
|
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
|
||||||
<div class="card-menu-items">
|
|
||||||
<a href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
|
|
||||||
{% call icons::location("h-4 w-4") %} View on map
|
|
||||||
</a>
|
|
||||||
{% if cafe.has_website %}
|
|
||||||
<a href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
|
|
||||||
{% call icons::external_link("h-4 w-4") %} Visit website
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if is_authenticated %}
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@
|
||||||
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
|
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
|
||||||
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
|
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
|
||||||
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
|
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
|
||||||
|
{% if is_authenticated %}
|
||||||
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
<th scope="col" class="px-4 py-3 text-right">Actions</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
|
|
@ -36,7 +38,8 @@
|
||||||
class="transition hover:bg-surface-alt"
|
class="transition hover:bg-surface-alt"
|
||||||
>
|
>
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ cup.created_at }}
|
<div>{{ cup.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ cup.created_time }}</div>
|
||||||
</td>
|
</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">
|
||||||
<div class="font-medium text-stone-800">{{ cup.roast_name }}</div>
|
<div class="font-medium text-stone-800">{{ cup.roast_name }}</div>
|
||||||
|
|
@ -46,31 +49,40 @@
|
||||||
<td data-label="Cafe" class="px-4 py-3 whitespace-nowrap">
|
<td data-label="Cafe" class="px-4 py-3 whitespace-nowrap">
|
||||||
{{ cup.cafe_name }}
|
{{ cup.cafe_name }}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
{% if is_authenticated %}
|
||||||
{% if is_authenticated %}
|
<td data-label="" class="card-detail hidden">
|
||||||
<div class="hidden md:inline-flex items-center gap-1">
|
<div class="flex items-center gap-4">
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||||
title="Delete cup"
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})"
|
</button>
|
||||||
>
|
</div>
|
||||||
<span class="sr-only">Delete</span>
|
</td>
|
||||||
{% call icons::delete("h-4 w-4") %}
|
<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-stone-400 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>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="4" 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-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<details class="card-menu md:hidden">
|
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
|
||||||
<div class="card-menu-items">
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
{% for item in gear.items %}
|
{% for item in gear.items %}
|
||||||
<tr class="transition hover:bg-surface-alt">
|
<tr class="transition hover:bg-surface-alt">
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ item.created_at }}
|
<div>{{ item.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ item.created_time }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="" class="card-title px-4 py-3 whitespace-nowrap md:hidden">{{ item.make }} {{ item.model }}</td>
|
<td data-label="" class="card-title px-4 py-3 whitespace-nowrap md:hidden">{{ item.make }} {{ item.model }}</td>
|
||||||
<td data-label="Category" class="px-4 py-3 whitespace-nowrap">
|
<td data-label="Category" class="px-4 py-3 whitespace-nowrap">
|
||||||
|
|
@ -50,28 +51,39 @@
|
||||||
{{ item.model }}
|
{{ item.model }}
|
||||||
</td>
|
</td>
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
|
<td data-label="" class="card-detail hidden">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||||
class="hidden md:inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
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="Delete gear"
|
title="Actions">
|
||||||
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})"
|
<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>
|
||||||
<span class="sr-only">Delete</span>
|
|
||||||
{% call icons::delete("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
<details class="card-menu md:hidden">
|
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
|
||||||
<div class="card-menu-items">
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="5" 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-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@
|
||||||
class="transition hover:bg-surface-alt"
|
class="transition hover:bg-surface-alt"
|
||||||
>
|
>
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ roast.created_at }}
|
<div>{{ roast.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ roast.created_time }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Roast" class="card-title px-4 py-3">
|
<td data-label="Roast" class="card-title px-4 py-3">
|
||||||
<div class="font-medium text-stone-800">{{ roast.name }}</div>
|
<div class="font-medium text-stone-800">{{ roast.name }}</div>
|
||||||
|
|
@ -60,36 +61,47 @@
|
||||||
{% if !roast.producer.is_empty() %}
|
{% if !roast.producer.is_empty() %}
|
||||||
<td data-label="Producer" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.producer }}</td>
|
<td data-label="Producer" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roast.producer }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td data-label="Tasting Notes" class="px-4 py-3 text-stone-600 text-right md:text-left">
|
<td data-label="Tasting Notes" class="px-4 py-3 text-xs text-stone-500 text-right md:text-left">
|
||||||
{% if roast.tasting_notes.is_empty() %}
|
{% if roast.tasting_notes.is_empty() %}
|
||||||
<span class="text-stone-500">No tasting notes yet.</span>
|
<span class="text-stone-400">—</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ roast.tasting_notes.join(", ") }}
|
{{ roast.tasting_notes.join(", ") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
|
<td data-label="" class="card-detail hidden">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||||
class="hidden md:inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
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="Delete roast"
|
title="Actions">
|
||||||
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})"
|
<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>
|
||||||
<span class="sr-only">Delete</span>
|
|
||||||
{% call icons::delete("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
<details class="card-menu md:hidden">
|
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
|
||||||
<div class="card-menu-items">
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="5" 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-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@
|
||||||
class="transition hover:bg-surface-alt"
|
class="transition hover:bg-surface-alt"
|
||||||
>
|
>
|
||||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 text-xs font-medium text-stone-600">
|
||||||
{{ roaster.created_at }}
|
<div>{{ roaster.created_date }}</div>
|
||||||
|
<div class="hidden md:block font-normal text-stone-400">{{ roaster.created_time }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
<td data-label="Name" class="card-title px-4 py-3 font-medium text-stone-800">
|
||||||
{{ roaster.name }}
|
{{ roaster.name }}
|
||||||
|
|
@ -54,52 +55,58 @@
|
||||||
{% if !roaster.city.is_empty() %}
|
{% if !roaster.city.is_empty() %}
|
||||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.city }}</td>
|
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ roaster.city }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
{% if roaster.has_homepage || is_authenticated %}
|
||||||
<div class="hidden md:inline-flex items-center gap-1">
|
<td data-label="" class="card-detail hidden">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
{% if roaster.has_homepage %}
|
{% if roaster.has_homepage %}
|
||||||
<a
|
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
|
||||||
href="{{ roaster.homepage_url }}"
|
{% call icons::external_link("h-4 w-4") %} Homepage
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
title="Visit homepage"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Visit {{ roaster.name }} homepage</span>
|
|
||||||
{% call icons::external_link("h-4 w-4") %}
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||||
title="Delete roaster"
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Delete</span>
|
|
||||||
{% call icons::delete("h-4 w-4") %}
|
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
{% if roaster.has_homepage || is_authenticated %}
|
{% if roaster.has_homepage || is_authenticated %}
|
||||||
<details class="card-menu md:hidden">
|
<button type="button"
|
||||||
<summary>{% call icons::ellipsis_vertical("h-5 w-5") %}</summary>
|
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-stone-400 transition hover:text-accent hover:bg-surface-alt"
|
||||||
<div class="card-menu-items">
|
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');"
|
||||||
{% if roaster.has_homepage %}
|
title="Actions">
|
||||||
<a href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
|
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
|
||||||
{% call icons::external_link("h-4 w-4") %} Visit homepage
|
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
|
||||||
</a>
|
</button>
|
||||||
{% endif %}
|
|
||||||
{% if is_authenticated %}
|
|
||||||
<button type="button" class="menu-item-danger"
|
|
||||||
data-on:click="this.closest('details').open = false; confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
|
||||||
{% call icons::delete("h-4 w-4") %} Delete
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if roaster.has_homepage || is_authenticated %}
|
||||||
|
<tr class="hidden detail-row">
|
||||||
|
<td colspan="4" 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"
|
||||||
|
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
|
||||||
|
{% call icons::external_link("h-4 w-4") %} Homepage
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_authenticated %}
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-red-600"
|
||||||
|
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||||
|
{% call icons::delete("h-4 w-4") %} Delete
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue