fix(brews): improve table layout and add "brew again" feature

- Combine weight/water/ratio into single "Recipe" column
- Reorder columns: Coffee, Grind, Recipe, Temp, Brewer, Date, Actions
- Style actions column with icon buttons (consistent with roasts)
- Add "brew again" button to repeat a brew with same parameters
- Use data-model for form inputs (fixes Datastar binding error)
- Add raw values to BrewView for repeat brew functionality
This commit is contained in:
Jon Seager 2026-02-02 19:49:22 +00:00
parent 7e579cb2d1
commit 9402d98bb6
No known key found for this signature in database
3 changed files with 43 additions and 7 deletions

View file

@ -585,19 +585,26 @@ impl GearView {
#[derive(Clone)] #[derive(Clone)]
pub struct BrewView { pub struct BrewView {
pub id: String, pub id: String,
pub bag_id: String, pub bag_id: i64,
pub roast_name: String, pub roast_name: String,
pub roaster_name: String, pub roaster_name: String,
pub roast_slug: String, pub roast_slug: String,
pub roaster_slug: String, pub roaster_slug: String,
pub coffee_weight: String, pub coffee_weight: String,
pub grinder_id: i64,
pub grinder_name: String, pub grinder_name: String,
pub grind_setting: String, pub grind_setting: String,
pub brewer_id: i64,
pub brewer_name: String, pub brewer_name: String,
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_at: String,
// Raw values for "brew again" feature
pub coffee_weight_raw: f64,
pub grind_setting_raw: f64,
pub water_volume_raw: i32,
pub water_temp_raw: f64,
} }
impl BrewView { impl BrewView {
@ -613,19 +620,25 @@ impl BrewView {
Self { Self {
id: brew.brew.id.to_string(), id: brew.brew.id.to_string(),
bag_id: brew.brew.bag_id.to_string(), bag_id: brew.brew.bag_id.into_inner(),
roast_name: brew.roast_name, roast_name: brew.roast_name,
roaster_name: brew.roaster_name, roaster_name: brew.roaster_name,
roast_slug: brew.roast_slug, roast_slug: brew.roast_slug,
roaster_slug: brew.roaster_slug, roaster_slug: brew.roaster_slug,
coffee_weight: format!("{:.1}g", brew.brew.coffee_weight), coffee_weight: format!("{:.1}g", brew.brew.coffee_weight),
grinder_id: brew.brew.grinder_id.into_inner(),
grinder_name: brew.grinder_name, grinder_name: brew.grinder_name,
grind_setting: format!("{:.1}", brew.brew.grind_setting), grind_setting: format!("{:.1}", brew.brew.grind_setting),
brewer_id: brew.brew.brewer_id.into_inner(),
brewer_name: brew.brewer_name, brewer_name: brew.brewer_name,
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_at: brew.brew.created_at.format("%Y-%m-%d %H:%M").to_string(),
coffee_weight_raw: brew.brew.coffee_weight,
grind_setting_raw: brew.brew.grind_setting,
water_volume_raw: brew.brew.water_volume,
water_temp_raw: brew.brew.water_temp,
} }
} }
} }

View file

@ -92,7 +92,7 @@
<span class="text-stone-700">Coffee (g) *</span> <span class="text-stone-700">Coffee (g) *</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$weight = Math.max(1, $weight - 1)">-</button> <button type="button" class="btn-adjust" data-on:click="$weight = Math.max(1, $weight - 1)">-</button>
<input type="number" name="coffee_weight" step="0.5" min="1" required class="input-field flex-1 text-center" value="15.0" data-bind:value="$weight" /> <input type="number" name="coffee_weight" step="0.5" min="1" required class="input-field flex-1 text-center" data-attr:value="$weight" data-on:input="$weight = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$weight = $weight + 1">+</button> <button type="button" class="btn-adjust" data-on:click="$weight = $weight + 1">+</button>
</div> </div>
</div> </div>
@ -102,7 +102,7 @@
<span class="text-stone-700">Grind Setting *</span> <span class="text-stone-700">Grind Setting *</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$grind = Math.max(0, $grind - 0.5)">-</button> <button type="button" class="btn-adjust" data-on:click="$grind = Math.max(0, $grind - 0.5)">-</button>
<input type="number" name="grind_setting" step="0.5" min="0" required class="input-field flex-1 text-center" value="6.0" data-bind:value="$grind" /> <input type="number" name="grind_setting" step="0.5" min="0" required class="input-field flex-1 text-center" data-attr:value="$grind" data-on:input="$grind = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$grind = $grind + 0.5">+</button> <button type="button" class="btn-adjust" data-on:click="$grind = $grind + 0.5">+</button>
</div> </div>
</div> </div>
@ -112,7 +112,7 @@
<span class="text-stone-700">Water (ml) *</span> <span class="text-stone-700">Water (ml) *</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$volume = Math.max(10, $volume - 10)">-</button> <button type="button" class="btn-adjust" data-on:click="$volume = Math.max(10, $volume - 10)">-</button>
<input type="number" name="water_volume" step="10" min="10" required class="input-field flex-1 text-center" value="250" data-bind:value="$volume" /> <input type="number" name="water_volume" step="10" min="10" required class="input-field flex-1 text-center" data-attr:value="$volume" data-on:input="$volume = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$volume = $volume + 10">+</button> <button type="button" class="btn-adjust" data-on:click="$volume = $volume + 10">+</button>
</div> </div>
</div> </div>
@ -122,7 +122,7 @@
<span class="text-stone-700">Temp (C) *</span> <span class="text-stone-700">Temp (C) *</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$temp = Math.max(0, $temp - 0.5)">-</button> <button type="button" class="btn-adjust" data-on:click="$temp = Math.max(0, $temp - 0.5)">-</button>
<input type="number" name="water_temp" step="0.5" min="0" max="100" required class="input-field flex-1 text-center" value="91.0" data-bind:value="$temp" /> <input type="number" name="water_temp" step="0.5" min="0" max="100" required class="input-field flex-1 text-center" data-attr:value="$temp" data-on:input="$temp = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$temp = Math.min(100, $temp + 0.5)">+</button> <button type="button" class="btn-adjust" data-on:click="$temp = Math.min(100, $temp + 0.5)">+</button>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@
<th scope="col" class="px-4 py-3">Brewer</th> <th scope="col" class="px-4 py-3">Brewer</th>
{% call table::sortable_header("Date", "created-at", navigator, "#brew-list") %} {% call table::sortable_header("Date", "created-at", navigator, "#brew-list") %}
{% if is_authenticated %} {% if is_authenticated %}
<th scope="col" class="px-4 py-3 text-right">Actions</th> <th scope="col" class="px-4 py-3 text-center">Actions</th>
{% endif %} {% endif %}
</tr> </tr>
</thead> </thead>
@ -47,6 +47,29 @@
<td class="px-4 py-3 whitespace-nowrap text-stone-500">{{ brew.created_at }}</td> <td class="px-4 py-3 whitespace-nowrap text-stone-500">{{ brew.created_at }}</td>
{% if is_authenticated %} {% if is_authenticated %}
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<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 }}" />
<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 h-8 w-8 items-center justify-center rounded-md text-stone-500 transition hover:text-amber-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500"
title="Brew again"
>
<span class="sr-only">Brew again</span>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0v2.43l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389 5.5 5.5 0 0 1 9.2-2.466l.312.311h-2.433a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.22Z"
clip-rule="evenodd"
/>
</svg>
</button>
</form>
<button <button
type="button" type="button"
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" 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"