brewlog/templates/pages/add.html
Jon Seager 202dafb9b9
feat(scan): show cards for existing roasters/roasts during bag scan
After AI extraction, check if the roaster and roast already exist by
slug. When matched, show compact summary cards instead of full edit
forms. Each card has a "Change" link to revert to the form if the
match is wrong.

- Add match_existing_entities() for slug-based roaster/roast lookup
- Return _matched-roaster-id and _matched-roast-id signals from extraction
- Add submit_existing_roast() path to skip creation when roast exists
- Dynamic submit buttons: Save Roaster & Roast / Save Roast / Open Bag
- Hidden inputs bound to signals handle all form submission cleanly
2026-02-06 12:27:25 +00:00

530 lines
27 KiB
HTML

{% extends "base.html" %} {% import "partials/icons.html" as icons %} {% import "partials/scan_input.html" as scan %} {% import "partials/location_search.html" as location %} {% block title %}Brewlog · Add{% endblock %}
{% block head %}
<script>
{% call location::location_search_js() %}
</script>
{% endblock %}
{% block content %}
<!-- Scan Bag (Quick Add) -->
<section
data-signals:_extracting="false"
data-signals:_extract-error="''"
data-signals:_scan-extracted="false"
data-signals:_scan-submitting="false"
data-signals:_scan-error="''"
data-signals:_roaster-name="''"
data-signals:_roaster-country="''"
data-signals:_roaster-city="''"
data-signals:_roaster-homepage="''"
data-signals:_roast-name="''"
data-signals:_origin="''"
data-signals:_region="''"
data-signals:_producer="''"
data-signals:_process="''"
data-signals:_tasting-notes="''"
data-signals:_open-bag="true"
data-signals:_bag-amount="250"
data-signals:_matched-roaster-id="''"
data-signals:_matched-roast-id="''"
>
<!-- Input: photo or text -->
<div data-show="!$_scanExtracted" class="rounded-lg border bg-surface p-5">
<form id="scan-extract-form"
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/extract-bag-scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_scanExtracted = true; document.getElementById('scan-extract-form').reset() } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Extraction failed. Please try again.' }"
>
{% call scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") %}
</form>
</div>
<!-- Scan result form -->
<div data-show="$_scanExtracted" style="display: none">
<form
class="rounded-lg border bg-surface p-5 flex flex-col gap-6"
data-on:submit="$_scanSubmitting = true; $_scanError = ''; @post('/api/v1/scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_scanSubmitting) return; if (evt.detail.type === 'finished') { $_scanSubmitting = false; window.location.href = '/data?type=bags' } else if (evt.detail.type === 'error') { $_scanSubmitting = false; $_scanError = 'Save failed. Please try again.' }"
>
{% include "partials/forms/scan_result_form.html" %}
</form>
</div>
</section>
<!-- Manual Add -->
<section
data-signals:_add-submitting="false"
data-signals:_add-roaster-name="''"
data-signals:_add-roaster-country="''"
data-signals:_add-roaster-city="''"
data-signals:_add-roaster-homepage="''"
data-signals:_add-roast-name="''"
data-signals:_add-origin="''"
data-signals:_add-region="''"
data-signals:_add-producer="''"
data-signals:_add-process="''"
data-signals:_add-tasting-notes="''"
data-signals:_brew-temp="{{ defaults.water_temp }}"
data-signals:_brew-grind="{{ defaults.grind_setting }}"
data-signals:_brew-volume="{{ defaults.water_volume }}"
data-signals:_brew-weight="{{ defaults.coffee_weight }}"
data-signals:_cafe-id="''"
data-signals:_cafe-name="''"
data-signals:_cafe-city="''"
data-signals:_cafe-country="''"
data-signals:_cafe-lat="''"
data-signals:_cafe-lng="''"
data-signals:_cafe-website="''"
data-signals:_cafe-search="''"
data-signals:_cafe-error="''"
data-signals:_city-name="''"
data-signals:_locating="false"
data-signals:_location-found="false"
data-signals:_user-lat="0"
data-signals:_user-lng="0"
data-signals:_reviewing-cafe="false"
data-signals:_qn-good="false"
data-signals:_qn-too-fast="false"
data-signals:_qn-too-slow="false"
data-signals:_qn-too-hot="false"
data-signals:_qn-under-extracted="false"
data-signals:_qn-over-extracted="false"
>
<h2 class="text-lg font-semibold text-text mb-3">Manual Add</h2>
<!-- Entity type selector -->
<div class="rounded-lg border bg-surface mb-4">
{% include "partials/tab_bar.html" %}
</div>
<!-- ========== ROASTER FORM ========== -->
<div data-show="$_addType === 'roaster'" style="display:none" class="rounded-lg border bg-surface p-5">
<div>
<h3 class="text-lg font-semibold text-text">New Roaster</h3>
<p class="mt-1 text-sm text-text-secondary">Provide the core details and Brewlog will keep track of everything for you.</p>
</div>
<form method="post" action="/api/v1/roasters" class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Name *</span>
<input type="text" name="name" required class="input-field" placeholder="Example Coffee Roasters" data-bind:_add-roaster-name />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Country *</span>
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_add-roaster-country />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">City</span>
<input type="text" name="city" class="input-field" placeholder="Portland" data-bind:_add-roaster-city />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Homepage</span>
<input type="url" name="homepage" class="input-field" placeholder="https://example.coffee" data-bind:_add-roaster-homepage />
</label>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Roaster
</button>
</div>
</form>
</div>
<!-- ========== ROAST FORM ========== -->
<div data-show="$_addType === 'roast'" style="display:none" class="rounded-lg border bg-surface p-5">
{% if roaster_options.is_empty() %}
<div class="text-sm text-text-secondary">
<h3 class="text-lg font-semibold text-text">Add a roaster first</h3>
<p class="mt-2">Roasts need a roaster. Add a roaster above to enable this form.</p>
</div>
{% else %}
<div>
<h3 class="text-lg font-semibold text-text">New Roast</h3>
<p class="mt-1 text-sm text-text-secondary">Select a roaster and fill in the details.</p>
</div>
<form method="post" action="/api/v1/roasts" class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Roaster *</span>
<searchable-select name="roaster_id" placeholder="Type to search roasters&hellip;">
{% for roaster in roaster_options %}
<button type="button" value="{{ roaster.id }}" data-display="{{ roaster.name }}"
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
<span class="font-medium text-text">{{ roaster.name }}</span>
</button>
{% endfor %}
</searchable-select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Roast Name *</span>
<input type="text" name="name" required class="input-field" placeholder="Ethiopia Yirgacheffe" data-bind:_add-roast-name />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Origin *</span>
<input type="text" name="origin" required class="input-field" placeholder="Ethiopia" data-bind:_add-origin />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Region *</span>
<input type="text" name="region" required class="input-field" placeholder="Guji" data-bind:_add-region />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Producer *</span>
<input type="text" name="producer" required class="input-field" placeholder="Chelbesa Cooperative" data-bind:_add-producer />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Process *</span>
<input type="text" name="process" required class="input-field" placeholder="Washed" data-bind:_add-process />
</label>
<label class="sm:col-span-2 flex flex-col gap-1 text-sm">
<span class="text-text">Tasting Notes * (comma or newline separated)</span>
<textarea name="tasting_notes" rows="2" required class="input-field" placeholder="Blueberry, Jasmine" data-bind:_add-tasting-notes></textarea>
</label>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Roast
</button>
</div>
</form>
{% endif %}
</div>
<!-- ========== BAG FORM ========== -->
<div data-show="$_addType === 'bag'" style="display:none" class="rounded-lg border bg-surface p-5">
{% if roast_options.is_empty() %}
<div class="text-sm text-text-secondary">
<h3 class="text-lg font-semibold text-text">Add a roast first</h3>
<p class="mt-2">Bags need a roast. Add a roaster and roast to enable this form.</p>
</div>
{% else %}
<div>
<h3 class="text-lg font-semibold text-text">New Bag</h3>
<p class="mt-1 text-sm text-text-secondary">Select a roast and enter the details.</p>
</div>
<form method="post" action="/api/v1/bags" class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Roast *</span>
<searchable-select name="roast_id" placeholder="Type to search roasts&hellip;">
{% for roast in roast_options %}
<button type="button" value="{{ roast.id }}" data-display="{{ roast.label }}"
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
<span class="font-medium text-text">{{ roast.name }}</span>
<span class="ml-2 text-xs text-text-muted">{{ roast.roaster_name }}</span>
</button>
{% endfor %}
</searchable-select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Roast Date</span>
<input type="date" name="roast_date" class="input-field" />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Amount (g) *</span>
<input type="number" name="amount" step="0.1" required class="input-field" placeholder="250" />
</label>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Bag
</button>
</div>
</form>
{% endif %}
</div>
<!-- ========== BREW FORM ========== -->
<div data-show="$_addType === 'brew'" style="display:none" class="rounded-lg border bg-surface p-5">
{% if bag_options.is_empty() %}
<div class="text-sm text-text-secondary">
<h3 class="text-lg font-semibold text-text">Open a bag first</h3>
<p class="mt-2">Brews need an open bag of coffee. Add a bag to enable this form.</p>
</div>
{% else if grinder_options.is_empty() || brewer_options.is_empty() %}
<div class="text-sm text-text-secondary">
<h3 class="text-lg font-semibold text-text">Add your gear first</h3>
<p class="mt-2">Brews need a grinder and brewer. Add your gear to enable this form.</p>
</div>
{% else %}
<div>
<h3 class="text-lg font-semibold text-text">New Brew</h3>
<p class="mt-1 text-sm text-text-secondary">Log a cup of coffee.</p>
</div>
<form method="post" action="/api/v1/brews" class="mt-4 flex flex-col gap-6 overflow-hidden">
<!-- Coffee -->
<div>
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Coffee</h4>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Bag *</span>
<searchable-select name="bag_id" placeholder="Type to search bags&hellip;"{% if let Some(bag_id) = pre_select_bag_id %} initial-value="{{ bag_id }}"{% endif %}>
{% for bag in bag_options %}
<button type="button" value="{{ bag.id }}" data-display="{{ bag.roast_name }}"
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
<span class="font-medium text-text">{{ bag.roast_name }}</span>
<span class="ml-2 text-xs text-text-muted">{{ bag.roaster_name }} &middot; {{ bag.remaining }}</span>
</button>
{% endfor %}
</searchable-select>
</div>
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Weight (g) *</span>
<div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$_brewWeight = Math.max(1, $_brewWeight - 0.5)">-</button>
<input type="number" name="coffee_weight" step="any" min="1" required class="input-field flex-1 text-center" data-attr:value="$_brewWeight" data-on:input="$_brewWeight = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$_brewWeight = $_brewWeight + 0.5">+</button>
</div>
</div>
</div>
</div>
<!-- Grinder -->
<div>
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Grinder</h4>
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Grinder *</span>
<select name="grinder_id" required class="input-field">
{% for grinder in grinder_options %}
<option value="{{ grinder.id }}"{% if grinder.id == defaults.grinder_id %} selected{% endif %}>{{ grinder.label }}</option>
{% endfor %}
</select>
</label>
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Grind Setting *</span>
<div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$_brewGrind = Math.max(0, $_brewGrind - 0.5)">-</button>
<input type="number" name="grind_setting" step="any" min="0" required class="input-field flex-1 text-center" data-attr:value="$_brewGrind" data-on:input="$_brewGrind = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$_brewGrind = $_brewGrind + 0.5">+</button>
</div>
</div>
</div>
</div>
<!-- Brewer -->
<div>
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Brewer</h4>
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Brewer *</span>
<select name="brewer_id" required class="input-field">
{% for brewer in brewer_options %}
<option value="{{ brewer.id }}"{% if brewer.id == defaults.brewer_id %} selected{% endif %}>{{ brewer.label }}</option>
{% endfor %}
</select>
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Filter Paper</span>
<select name="filter_paper_id" class="input-field">
<option value="">None</option>
{% for fp in filter_paper_options %}
<option value="{{ fp.id }}"{% if fp.id == defaults.filter_paper_id %} selected{% endif %}>{{ fp.label }}</option>
{% endfor %}
</select>
</label>
</div>
</div>
<!-- Water -->
<div>
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Water</h4>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Volume (ml) *</span>
<div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$_brewVolume = Math.max(10, $_brewVolume - 10)">-</button>
<input type="number" name="water_volume" step="any" min="10" required class="input-field flex-1 text-center" data-attr:value="$_brewVolume" data-on:input="$_brewVolume = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$_brewVolume = $_brewVolume + 10">+</button>
</div>
</div>
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Temp (&deg;C) *</span>
<div class="flex items-center gap-2">
<button type="button" class="btn-adjust" data-on:click="$_brewTemp = Math.max(0, $_brewTemp - 0.5)">-</button>
<input type="number" name="water_temp" step="any" min="0" max="100" required class="input-field flex-1 text-center" data-attr:value="$_brewTemp" data-on:input="$_brewTemp = this.valueAsNumber" />
<button type="button" class="btn-adjust" data-on:click="$_brewTemp = Math.min(100, $_brewTemp + 0.5)">+</button>
</div>
</div>
</div>
</div>
<!-- Quick Notes -->
<div>
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Quick Notes</h4>
<div class="flex flex-wrap gap-2">
<button type="button"
data-on:click="$_qnGood = !$_qnGood"
data-attr:class="$_qnGood ? 'pill pill-success cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Good</button>
<button type="button"
data-on:click="$_qnTooFast = !$_qnTooFast"
data-attr:class="$_qnTooFast ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Fast</button>
<button type="button"
data-on:click="$_qnTooSlow = !$_qnTooSlow"
data-attr:class="$_qnTooSlow ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Slow</button>
<button type="button"
data-on:click="$_qnTooHot = !$_qnTooHot"
data-attr:class="$_qnTooHot ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Too Hot</button>
<button type="button"
data-on:click="$_qnUnderExtracted = !$_qnUnderExtracted"
data-attr:class="$_qnUnderExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Under Extracted</button>
<button type="button"
data-on:click="$_qnOverExtracted = !$_qnOverExtracted"
data-attr:class="$_qnOverExtracted ? 'pill pill-warning cursor-pointer select-none transition' : 'pill pill-muted cursor-pointer select-none transition'">Over Extracted</button>
</div>
<input type="hidden" name="quick_notes"
data-attr:value="[$_qnGood && 'good', $_qnTooFast && 'too-fast', $_qnTooSlow && 'too-slow', $_qnTooHot && 'too-hot', $_qnUnderExtracted && 'under-extracted', $_qnOverExtracted && 'over-extracted'].filter(Boolean).join(',')" />
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Log Brew
</button>
</div>
</form>
{% endif %}
</div>
<!-- ========== GEAR FORM ========== -->
<div data-show="$_addType === 'gear'" style="display:none" class="rounded-lg border bg-surface p-5">
<div>
<h3 class="text-lg font-semibold text-text">New Gear</h3>
<p class="mt-1 text-sm text-text-secondary">Add brewing equipment to your collection.</p>
</div>
<form method="post" action="/api/v1/gear" class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Category *</span>
<select name="category" required class="input-field">
<option value="">Select a category</option>
<option value="grinder">Grinder</option>
<option value="brewer">Brewer</option>
<option value="filter_paper">Filter Paper</option>
</select>
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Make *</span>
<input type="text" name="make" required class="input-field" placeholder="Baratza" />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Model *</span>
<input type="text" name="model" required class="input-field" placeholder="Encore" />
</label>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Gear
</button>
</div>
</form>
</div>
<!-- ========== CAFE FORM ========== -->
<div data-show="$_addType === 'cafe'" style="display:none" class="rounded-lg border bg-surface p-5">
<div>
<h3 class="text-lg font-semibold text-text">New Cafe</h3>
<p class="mt-1 text-sm text-text-secondary">Search for a nearby cafe or enter details manually.</p>
</div>
<!-- Location search -->
<div class="mt-4 border-b pb-4" data-show="!$_reviewingCafe">
{% call location::location_search("$_cafeError") %}
<p data-show="$_cafeError" data-text="$_cafeError" style="display:none" class="mt-2 text-sm text-red-600"></p>
</div>
<!-- Selected cafe indicator -->
<div class="mt-4 border-b pb-4 flex items-center justify-between" data-show="$_reviewingCafe" style="display:none">
<div class="flex items-center gap-2">
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
<span class="text-sm font-medium text-text" data-text="$_cafeName"></span>
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
</div>
<button type="button"
class="text-xs text-text-muted hover:text-text"
data-on:click="$_reviewingCafe = false; $_cafeName = ''; $_cafeCity = ''; $_cafeCountry = ''; $_cafeLat = ''; $_cafeLng = ''; $_cafeWebsite = ''"
>Back</button>
</div>
<form id="cafe-form" method="post" action="/api/v1/cafes" class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Name *</span>
<input type="text" name="name" required class="input-field" placeholder="Blue Bottle Coffee" data-bind:_cafe-name />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">City *</span>
<input type="text" name="city" required class="input-field" placeholder="San Francisco" data-bind:_cafe-city />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Country *</span>
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_cafe-country />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Website</span>
<input type="url" name="website" class="input-field" placeholder="https://bluebottlecoffee.com" data-bind:_cafe-website />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Latitude *</span>
<input type="number" name="latitude" step="any" required class="input-field" placeholder="37.7749" data-bind:_cafe-lat />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Longitude *</span>
<input type="number" name="longitude" step="any" required class="input-field" placeholder="-122.4194" data-bind:_cafe-lng />
</label>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Cafe
</button>
</div>
</form>
</div>
<!-- ========== CUP FORM ========== -->
<div data-show="$_addType === 'cup'" style="display:none" class="rounded-lg border bg-surface p-5">
{% if roast_options.is_empty() || cafe_options.is_empty() %}
<div class="text-sm text-text-secondary">
<h3 class="text-lg font-semibold text-text">Add a roast and cafe first</h3>
<p class="mt-2">Cups need both a roast and a cafe. Add them above to enable this form.</p>
</div>
{% else %}
<div>
<h3 class="text-lg font-semibold text-text">New Cup</h3>
<p class="mt-1 text-sm text-text-secondary">Record a coffee you had at a cafe.</p>
</div>
<form method="post" action="/api/v1/cups" class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Coffee *</span>
<searchable-select name="roast_id" placeholder="Type to search roasts&hellip;">
{% for roast in roast_options %}
<button type="button" value="{{ roast.id }}" data-display="{{ roast.name }}"
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
<span class="font-medium text-text">{{ roast.name }}</span>
<span class="ml-2 text-xs text-text-muted">{{ roast.roaster_name }}</span>
</button>
{% endfor %}
</searchable-select>
</div>
<div class="flex flex-col gap-1 text-sm">
<span class="text-text">Cafe *</span>
<searchable-select name="cafe_id" placeholder="Type to search cafes&hellip;">
{% for cafe in cafe_options %}
<button type="button" value="{{ cafe.id }}" data-display="{{ cafe.name }}"
class="w-full px-3 py-2 text-left text-sm hover:bg-surface-alt transition">
<span class="font-medium text-text">{{ cafe.name }}</span>
<span class="ml-2 text-xs text-text-muted">{{ cafe.city }}</span>
</button>
{% endfor %}
</searchable-select>
</div>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
Save Cup
</button>
</div>
</form>
{% endif %}
</div>
</section>
{% endblock %}