feat(add): add extraction bars to roaster and roast forms
- Remove top-level scan section and scan_input import - Add per-form extraction bars with camera + describe input - Rename form signals from _add-* prefix to match extraction endpoints - Add per-form extracting/error signals to guard datastar-fetch events - Make Log Brew button full width
This commit is contained in:
parent
dd3714f0a1
commit
411d520cb3
1 changed files with 80 additions and 64 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
{% 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 %}
|
{% extends "base.html" %} {% import "partials/icons.html" as icons %} {% import "partials/location_search.html" as location %} {% block title %}Brewlog · Add{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -7,63 +7,22 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Scan Bag (Quick Add) -->
|
|
||||||
<section
|
<section
|
||||||
data-signals:_extracting="false"
|
data-signals:_add-submitting="false"
|
||||||
data-signals:_extract-error="''"
|
data-signals:_roaster-extracting="false"
|
||||||
data-signals:_scan-extracted="false"
|
data-signals:_roaster-extract-error="''"
|
||||||
data-signals:_scan-submitting="false"
|
|
||||||
data-signals:_scan-error="''"
|
|
||||||
data-signals:_roaster-name="''"
|
data-signals:_roaster-name="''"
|
||||||
data-signals:_roaster-country="''"
|
data-signals:_roaster-country="''"
|
||||||
data-signals:_roaster-city="''"
|
data-signals:_roaster-city="''"
|
||||||
data-signals:_roaster-homepage="''"
|
data-signals:_roaster-homepage="''"
|
||||||
|
data-signals:_roast-extracting="false"
|
||||||
|
data-signals:_roast-extract-error="''"
|
||||||
data-signals:_roast-name="''"
|
data-signals:_roast-name="''"
|
||||||
data-signals:_origin="''"
|
data-signals:_origin="''"
|
||||||
data-signals:_region="''"
|
data-signals:_region="''"
|
||||||
data-signals:_producer="''"
|
data-signals:_producer="''"
|
||||||
data-signals:_process="''"
|
data-signals:_process="''"
|
||||||
data-signals:_tasting-notes="''"
|
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.' }"
|
|
||||||
>
|
|
||||||
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe a 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-temp="{{ defaults.water_temp }}"
|
||||||
data-signals:_brew-grind="{{ defaults.grind_setting }}"
|
data-signals:_brew-grind="{{ defaults.grind_setting }}"
|
||||||
data-signals:_brew-volume="{{ defaults.water_volume }}"
|
data-signals:_brew-volume="{{ defaults.water_volume }}"
|
||||||
|
|
@ -96,10 +55,13 @@
|
||||||
data-signals:_qn-under-extracted="{% if defaults.quick_notes_raw.contains("under-extracted") %}true{% else %}false{% endif %}"
|
data-signals:_qn-under-extracted="{% if defaults.quick_notes_raw.contains("under-extracted") %}true{% else %}false{% endif %}"
|
||||||
data-signals:_qn-over-extracted="{% if defaults.quick_notes_raw.contains("over-extracted") %}true{% else %}false{% endif %}"
|
data-signals:_qn-over-extracted="{% if defaults.quick_notes_raw.contains("over-extracted") %}true{% else %}false{% endif %}"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold text-text mb-3">Manual Add</h2>
|
<header class="flex flex-col gap-2 mb-6">
|
||||||
|
<h1 class="text-3xl font-semibold">Add</h1>
|
||||||
|
<p class="max-w-2xl text-sm text-text-secondary">Add new coffee data manually or by scanning a bag.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Entity type selector -->
|
<!-- Entity type selector -->
|
||||||
<div class="rounded-lg border bg-surface mb-4">
|
<div class="mb-4">
|
||||||
{% include "partials/tab_bar.html" %}
|
{% include "partials/tab_bar.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -107,26 +69,52 @@
|
||||||
<div data-show="$_addType === 'roaster'" style="display:none" class="rounded-lg border bg-surface p-5">
|
<div data-show="$_addType === 'roaster'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-text">New Roaster</h3>
|
<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>
|
<p class="mt-1 text-sm text-text-secondary">Describe or take a photo of a coffee bag to extract roaster details.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/api/v1/roasters" class="mt-4 flex flex-col gap-4">
|
<!-- Extraction bar -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<input type="hidden" name="image" id="roaster-extract-image" form="roaster-extract-form" />
|
||||||
|
<form id="roaster-extract-form"
|
||||||
|
data-on:submit="$_roasterExtracting = true; $_roasterExtractError = ''; @post('/api/v1/extract-roaster', {contentType: 'form'})"
|
||||||
|
data-on:datastar-fetch="if (!$_roasterExtracting) return; if (evt.detail.type === 'finished') { $_roasterExtracting = false; document.getElementById('roaster-extract-form').reset() } else if (evt.detail.type === 'error') { $_roasterExtracting = false; $_roasterExtractError = 'Extraction failed. Please try again.' }"
|
||||||
|
class="hidden"></form>
|
||||||
|
<div data-show="!$_roasterExtracting" class="flex items-center gap-2">
|
||||||
|
<brew-photo-capture target-input="roaster-extract-image" target-form="roaster-extract-form"
|
||||||
|
class="shrink-0 inline-flex items-center justify-center rounded-md border p-2.5 text-accent transition hover:bg-surface-alt cursor-pointer"
|
||||||
|
aria-label="Take Photo"
|
||||||
|
>
|
||||||
|
{{ icons::camera("h-5 w-5") }}
|
||||||
|
</brew-photo-capture>
|
||||||
|
<span class="text-xs text-text-muted" style="font-variant: small-caps">or</span>
|
||||||
|
<input type="text" name="prompt" form="roaster-extract-form" class="input-field w-full" placeholder="Describe a roaster…" />
|
||||||
|
</div>
|
||||||
|
<div data-show="$_roasterExtracting" style="display:none" class="flex items-center gap-3 text-sm text-accent">
|
||||||
|
{{ icons::spinner("h-5 w-5") }}
|
||||||
|
Extracting…
|
||||||
|
</div>
|
||||||
|
<p data-show="$_roasterExtractError" data-text="$_roasterExtractError" style="display:none" class="mt-2 text-sm text-red-600"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 border-t pt-3">
|
||||||
|
<p class="mb-2 text-sm text-text-secondary">Or add manually:</p>
|
||||||
|
<form method="post" action="/api/v1/roasters" class="flex flex-col gap-4">
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Name*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Name*</span>
|
||||||
<input type="text" name="name" required class="input-field" placeholder="Example Coffee Roasters" data-bind:_add-roaster-name />
|
<input type="text" name="name" required class="input-field" placeholder="Example Coffee Roasters" data-bind:_roaster-name />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Country*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Country*</span>
|
||||||
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_add-roaster-country />
|
<input type="text" name="country" required class="input-field" placeholder="United States" data-bind:_roaster-country />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">City</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">City</span>
|
||||||
<input type="text" name="city" class="input-field" placeholder="Portland" data-bind:_add-roaster-city />
|
<input type="text" name="city" class="input-field" placeholder="Portland" data-bind:_roaster-city />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Homepage</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Homepage</span>
|
||||||
<input type="url" name="homepage" class="input-field" placeholder="https://example.coffee" data-bind:_add-roaster-homepage />
|
<input type="url" name="homepage" class="input-field" placeholder="https://example.coffee" data-bind:_roaster-homepage />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
|
|
@ -136,6 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ========== ROAST FORM ========== -->
|
<!-- ========== ROAST FORM ========== -->
|
||||||
<div data-show="$_addType === 'roast'" style="display:none" class="rounded-lg border bg-surface p-5">
|
<div data-show="$_addType === 'roast'" style="display:none" class="rounded-lg border bg-surface p-5">
|
||||||
|
|
@ -147,10 +136,36 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-text">New Roast</h3>
|
<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>
|
<p class="mt-1 text-sm text-text-secondary">Describe or take a photo of a coffee bag to extract roast details.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/api/v1/roasts" class="mt-4 flex flex-col gap-4">
|
<!-- Extraction bar -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<input type="hidden" name="image" id="roast-extract-image" form="roast-extract-form" />
|
||||||
|
<form id="roast-extract-form"
|
||||||
|
data-on:submit="$_roastExtracting = true; $_roastExtractError = ''; @post('/api/v1/extract-roast', {contentType: 'form'})"
|
||||||
|
data-on:datastar-fetch="if (!$_roastExtracting) return; if (evt.detail.type === 'finished') { $_roastExtracting = false; document.getElementById('roast-extract-form').reset() } else if (evt.detail.type === 'error') { $_roastExtracting = false; $_roastExtractError = 'Extraction failed. Please try again.' }"
|
||||||
|
class="hidden"></form>
|
||||||
|
<div data-show="!$_roastExtracting" class="flex items-center gap-2">
|
||||||
|
<brew-photo-capture target-input="roast-extract-image" target-form="roast-extract-form"
|
||||||
|
class="shrink-0 inline-flex items-center justify-center rounded-md border p-2.5 text-accent transition hover:bg-surface-alt cursor-pointer"
|
||||||
|
aria-label="Take Photo"
|
||||||
|
>
|
||||||
|
{{ icons::camera("h-5 w-5") }}
|
||||||
|
</brew-photo-capture>
|
||||||
|
<span class="text-xs text-text-muted" style="font-variant: small-caps">or</span>
|
||||||
|
<input type="text" name="prompt" form="roast-extract-form" class="input-field w-full" placeholder="Describe a coffee bag…" />
|
||||||
|
</div>
|
||||||
|
<div data-show="$_roastExtracting" style="display:none" class="flex items-center gap-3 text-sm text-accent">
|
||||||
|
{{ icons::spinner("h-5 w-5") }}
|
||||||
|
Extracting…
|
||||||
|
</div>
|
||||||
|
<p data-show="$_roastExtractError" data-text="$_roastExtractError" style="display:none" class="mt-2 text-sm text-red-600"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 border-t pt-3">
|
||||||
|
<p class="mb-2 text-sm text-text-secondary">Or add manually:</p>
|
||||||
|
<form method="post" action="/api/v1/roasts" class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-1 text-sm">
|
<div class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Roaster*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Roaster*</span>
|
||||||
<searchable-select name="roaster_id" placeholder="Type to search roasters…">
|
<searchable-select name="roaster_id" placeholder="Type to search roasters…">
|
||||||
|
|
@ -165,27 +180,27 @@
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Roast Name*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Roast Name*</span>
|
||||||
<input type="text" name="name" required class="input-field" placeholder="Ethiopia Yirgacheffe" data-bind:_add-roast-name />
|
<input type="text" name="name" required class="input-field" placeholder="Ethiopia Yirgacheffe" data-bind:_roast-name />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Origin*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Origin*</span>
|
||||||
<input type="text" name="origin" required class="input-field" placeholder="Ethiopia" data-bind:_add-origin />
|
<input type="text" name="origin" required class="input-field" placeholder="Ethiopia" data-bind:_origin />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Region*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Region*</span>
|
||||||
<input type="text" name="region" required class="input-field" placeholder="Guji" data-bind:_add-region />
|
<input type="text" name="region" required class="input-field" placeholder="Guji" data-bind:_region />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Producer*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Producer*</span>
|
||||||
<input type="text" name="producer" required class="input-field" placeholder="Chelbesa Cooperative" data-bind:_add-producer />
|
<input type="text" name="producer" required class="input-field" placeholder="Chelbesa Cooperative" data-bind:_producer />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1 text-sm">
|
<label class="flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Process*</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Process*</span>
|
||||||
<input type="text" name="process" required class="input-field" placeholder="Washed" data-bind:_add-process />
|
<input type="text" name="process" required class="input-field" placeholder="Washed" data-bind:_process />
|
||||||
</label>
|
</label>
|
||||||
<label class="sm:col-span-2 flex flex-col gap-1 text-sm">
|
<label class="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||||
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">Tasting Notes* (comma or newline separated)</span>
|
<span class="text-xs font-semibold text-text-muted uppercase tracking-wide">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>
|
<textarea name="tasting_notes" rows="2" required class="input-field" placeholder="Blueberry, Jasmine" data-bind:_tasting-notes></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
|
|
@ -194,6 +209,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -421,7 +437,7 @@
|
||||||
data-attr:value="[$_qnGood && 'good', $_qnTooFast && 'too-fast', $_qnTooSlow && 'too-slow', $_qnTooHot && 'too-hot', $_qnUnderExtracted && 'under-extracted', $_qnOverExtracted && 'over-extracted'].filter(Boolean).join(',')" />
|
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>
|
||||||
<div class="sticky-submit flex items-center justify-end">
|
<div class="sticky-submit 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 md:w-auto w-full">
|
<button type="submit" class="w-full rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover">
|
||||||
Log Brew
|
Log Brew
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue