feat(ui): add horizontal scrolling recent brews on homepage
- Load last 10 brews instead of 3, display in horizontal scroll - Extract brew card into reusable partial with portrait layout - Add grinder_model field to BrewView for compact card display - Move brew-again button to top-right icon, truncate long names
This commit is contained in:
parent
55e549ceac
commit
577c0e39d3
5 changed files with 63 additions and 72 deletions
|
|
@ -57,16 +57,12 @@ fn count_request<K: SortKey>() -> ListRequest<K> {
|
|||
async fn load_home_content(state: &AppState) -> Result<HomeContent, AppError> {
|
||||
let recent_brews_req = ListRequest::new(
|
||||
1,
|
||||
PageSize::limited(3),
|
||||
PageSize::limited(10),
|
||||
BrewSortKey::CreatedAt,
|
||||
SortDirection::Desc,
|
||||
);
|
||||
let open_bags_req = ListRequest::new(
|
||||
1,
|
||||
PageSize::All,
|
||||
BagSortKey::UpdatedAt,
|
||||
SortDirection::Desc,
|
||||
);
|
||||
let open_bags_req =
|
||||
ListRequest::new(1, PageSize::All, BagSortKey::UpdatedAt, SortDirection::Desc);
|
||||
let recent_events_req = ListRequest::new(
|
||||
1,
|
||||
PageSize::limited(5),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ pub struct BrewView {
|
|||
pub coffee_weight: String,
|
||||
pub grinder_id: i64,
|
||||
pub grinder_name: String,
|
||||
pub grinder_model: String,
|
||||
pub grind_setting: String,
|
||||
pub brewer_id: i64,
|
||||
pub brewer_name: String,
|
||||
|
|
@ -93,6 +94,10 @@ impl BrewView {
|
|||
roaster_slug: brew.roaster_slug,
|
||||
coffee_weight: format!("{:.1}g", brew.brew.coffee_weight),
|
||||
grinder_id: brew.brew.grinder_id.into_inner(),
|
||||
grinder_model: brew
|
||||
.grinder_name
|
||||
.split_once(' ')
|
||||
.map_or_else(|| brew.grinder_name.clone(), |(_, model)| model.to_string()),
|
||||
grinder_name: brew.grinder_name,
|
||||
grind_setting: format!("{:.1}", brew.brew.grind_setting),
|
||||
brewer_id: brew.brew.brewer_id.into_inner(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "base.html" %} {% import "partials/bag_card.html" as bag_card %} {% import
|
||||
"partials/brew_card.html" as brew_card %} {% import
|
||||
"partials/icons.html" as icons %} {% import "partials/scan_input.html" as scan %} {% block title
|
||||
%}Brewlog{% endblock %} {% block head %} {% if is_authenticated %}
|
||||
<script>
|
||||
|
|
@ -81,70 +82,9 @@
|
|||
>View all brews →</a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 items-start gap-4">
|
||||
<div class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory scrollbar-hide">
|
||||
{% for brew in recent_brews %}
|
||||
<div class="rounded-lg border bg-surface p-4 min-w-0">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span class="block truncate font-semibold text-text">{{ brew.roast_name }}</span>
|
||||
<p class="truncate text-sm text-text-muted">{{ brew.roaster_name }}</p>
|
||||
</div>
|
||||
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
|
||||
>{{ brew.relative_date_label }}</time
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 space-y-0.5 text-sm">
|
||||
<p class="truncate">
|
||||
<span class="font-medium text-text"
|
||||
>{{ brew.coffee_weight }} · {{ brew.water_volume }} · {{ brew.water_temp }}</span
|
||||
>
|
||||
<span class="text-text-muted">· {{ brew.ratio }}</span>
|
||||
</p>
|
||||
<p class="truncate">
|
||||
<span class="font-medium text-text">{{ brew.grind_setting }}</span>
|
||||
<span class="text-text-muted">· {{ brew.grinder_name }}</span>
|
||||
</p>
|
||||
<p class="truncate">
|
||||
<span class="font-medium text-text">{{ brew.brewer_name }}</span>{% if let Some(fp) =
|
||||
brew.filter_paper_name %} <span class="text-text-muted">· {{ fp }}</span>{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t flex items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% if !brew.quick_notes.is_empty() %} {% for qn in brew.quick_notes %}
|
||||
<span class="{{ qn.pill_class }}">{{ qn.label }}</span>
|
||||
{% endfor %} {% else %}
|
||||
<span class="pill pill-muted">No Notes</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_authenticated %}
|
||||
<form
|
||||
class="inline shrink-0"
|
||||
data-on:submit="$_brewing = true; @post('/api/v1/brews', {contentType: 'form'})"
|
||||
data-on:datastar-fetch="if (!$_brewing) return; if (evt.detail.type === 'finished') { window.location.reload() } else if (evt.detail.type === 'error') { $_brewing = false }"
|
||||
>
|
||||
<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 }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-accent transition hover:text-accent-hover hover:bg-surface-alt"
|
||||
title="Brew Again"
|
||||
>
|
||||
{{ icons::refresh("h-4 w-4") }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ brew_card::card(brew, is_authenticated) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
</button>
|
||||
{% endif %}
|
||||
<div class="flex-1 pr-5">
|
||||
<span class="block font-semibold text-text">{{ bag.roast_name }}</span>
|
||||
<p class="mt-1 text-sm text-text-muted">{{ bag.roaster_name }}</p>
|
||||
<span class="block font-semibold text-text truncate">{{ bag.roast_name }}</span>
|
||||
<p class="mt-1 text-sm text-text-muted truncate">{{ bag.roaster_name }}</p>
|
||||
</div>
|
||||
{% if is_authenticated %}
|
||||
<div class="mb-4 flex items-center">
|
||||
|
|
|
|||
50
templates/partials/brew_card.html
Normal file
50
templates/partials/brew_card.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{% import "partials/icons.html" as icons %}
|
||||
{% macro card(brew, is_authenticated) %}
|
||||
<div class="relative w-[45vw] md:w-[200px] h-[280px] shrink-0 snap-start rounded-lg border bg-surface p-4 flex flex-col">
|
||||
{% if is_authenticated %}
|
||||
<form
|
||||
class="absolute top-4 right-3"
|
||||
data-on:submit="$_brewing = true; @post('/api/v1/brews', {contentType: 'form'})"
|
||||
data-on:datastar-fetch="if (!$_brewing) return; if (evt.detail.type === 'finished') { window.location.reload() } else if (evt.detail.type === 'error') { $_brewing = false }"
|
||||
>
|
||||
<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 }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Brew Again"
|
||||
>
|
||||
{{ icons::refresh("h-4.5 w-4.5") }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="h-[4.5rem] overflow-hidden pr-5">
|
||||
<span class="block font-semibold text-text truncate">{{ brew.roast_name }}</span>
|
||||
<p class="mt-0.5 text-sm text-text-muted truncate">{{ brew.roaster_name }}</p>
|
||||
</div>
|
||||
<div class="mt-2 flex-1 space-y-1 text-text-secondary" style="font-size: 0.8125rem">
|
||||
<p>{{ brew.coffee_weight }} · {{ brew.water_volume }}</p>
|
||||
<p>{{ brew.grinder_model }} · {{ brew.grind_setting }}</p>
|
||||
<p>{{ brew.brewer_name }}</p>
|
||||
{% if let Some(fp) = brew.filter_paper_name %}<p>{{ fp }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% if !brew.quick_notes.is_empty() %}
|
||||
{% for qn in brew.quick_notes %}
|
||||
<span class="{{ qn.pill_class }}" style="font-size: 0.65rem">{{ qn.label }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="pill pill-muted" style="font-size: 0.65rem">No Notes</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Loading…
Reference in a new issue