brewlog/templates/pages/checkin.html
Jon Seager eaf558a524
feat(routes): redirect to detail page after creating brews and cups
Update create_brew, create_cup, and submit_checkin to navigate to the
new detail pages instead of reloading or redirecting to home.
2026-02-08 16:10:11 +00:00

328 lines
13 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 · Check In{% endblock %} {% block head %}
<script>
{{ location::location_search_js() }}
</script>
{% endblock %} {% block content %}
<section
id="checkin-root"
data-signals:_step="1"
data-signals:_cafe-id="''"
data-signals:_cafe-name="''"
data-signals:_cafe-city="''"
data-signals:_cafe-country="''"
data-signals:_cafe-lat="0"
data-signals:_cafe-lng="0"
data-signals:_cafe-website="''"
data-signals:_cafe-search="''"
data-signals:_roast-id="''"
data-signals:_roast-name="''"
data-signals:_roaster-name="''"
data-signals:_error="''"
data-signals:_submitting="false"
data-signals:_locating="false"
data-signals:_location-found="false"
data-signals:_user-lat="0"
data-signals:_user-lng="0"
data-signals:_city-name="''"
data-signals:_reviewing-cafe="false"
data-signals:_extracting="false"
data-signals:_extract-error="''"
data-signals:_scan-success="''"
>
<header class="flex flex-col gap-2">
<h1 class="text-3xl font-semibold">Check In</h1>
<p class="max-w-2xl text-sm text-text-secondary">Record a cup of coffee at a cafe.</p>
</header>
<!-- Step progress -->
<div class="relative mx-auto mt-6" style="width: 80%">
<!-- Background track (continuous line through dot centers) -->
<div class="absolute bg-border" style="height: 3px; top: 8px; left: 10px; right: 10px"></div>
<!-- Accent overlay: first segment -->
<div
class="absolute bg-accent"
style="height: 3px; top: 8px; left: 10px; right: 50%; display: none"
data-show="$_step > 1"
></div>
<!-- Accent overlay: second segment -->
<div
class="absolute bg-accent"
style="height: 3px; top: 8px; left: 50%; right: 10px; display: none"
data-show="$_step > 2"
></div>
<div class="relative flex justify-between">
<div class="flex flex-col items-center">
<div
class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
data-class="{'border-accent': $_step >= 1, 'border-border': $_step < 1}"
></div>
<span
class="mt-2 text-xs font-medium transition-colors"
data-class="{'text-accent': $_step === 1, 'text-text': $_step > 1}"
>Cafe</span
>
</div>
<div class="flex flex-col items-center">
<div
class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
data-class="{'border-accent': $_step >= 2, 'border-border': $_step < 2}"
></div>
<span
class="mt-2 text-xs font-medium transition-colors"
data-class="{'text-accent': $_step === 2, 'text-text': $_step > 2, 'text-text-muted': $_step < 2}"
>Coffee</span
>
</div>
<div class="flex flex-col items-center">
<div
class="h-5 w-5 rounded-full border-[3px] bg-page transition-all"
data-class="{'border-accent': $_step >= 3, 'border-border': $_step < 3}"
></div>
<span
class="mt-2 text-xs font-medium transition-colors"
data-class="{'text-accent': $_step === 3, 'text-text-muted': $_step < 3}"
>Submit</span
>
</div>
</div>
</div>
<!-- Selected cafe summary (shown after selection) -->
<div
class="mt-4 rounded-lg border bg-surface px-4 py-3 flex items-center justify-between cursor-pointer"
data-show="$_step > 1 && $_cafeName && !$_reviewingCafe"
style="display: none"
data-on:click="$_cafeId = ''; $_cafeName = ''; $_step = 1"
>
<div class="flex items-center gap-2">
{{ icons::location("h-4 w-4 text-accent shrink-0") }}
<span class="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"
data-on:click="$_cafeId = ''; $_cafeName = ''; $_step = 1"
class="inline-flex items-center gap-1 text-xs text-text-muted hover:text-text"
>
{{ icons::pencil("h-3 w-3") }} Change
</button>
</div>
<!-- Selected roast summary (shown after selection) -->
<div
class="mt-2 rounded-lg border bg-surface px-4 py-3 flex items-center justify-between cursor-pointer"
data-show="$_roastName && $_step > 2"
style="display: none"
data-on:click="$_roastId = ''; $_roastName = ''; $_roasterName = ''; $_scanSuccess = ''; $_step = 2"
>
<div class="flex items-center gap-2">
{{ icons::bag("h-4 w-4 text-accent shrink-0") }}
<span class="font-medium text-text" data-text="$_roastName"></span>
<span
class="text-xs text-text-muted"
data-show="$_roasterName"
data-text="$_roasterName"
style="display: none"
></span>
</div>
<button
type="button"
data-on:click="$_roastId = ''; $_roastName = ''; $_roasterName = ''; $_scanSuccess = ''; $_step = 2"
class="inline-flex items-center gap-1 text-xs text-text-muted hover:text-text"
>
{{ icons::pencil("h-3 w-3") }} Change
</button>
</div>
<!-- Error display -->
<p
class="mt-3 text-sm text-red-600"
data-show="$_error"
style="display: none"
data-text="$_error"
></p>
<!-- Step 1: Cafe selection -->
<div class="mt-4" data-show="$_step === 1" style="display: none">
<div class="rounded-lg border bg-surface p-5">
<div data-show="!$_reviewingCafe">{{ location::location_search("$_error") }}</div>
<!-- Review form for new cafe from Foursquare -->
<div data-show="$_reviewingCafe" style="display: none">
<h3 class="text-base font-semibold text-text mb-1">Confirm cafe details</h3>
<p class="text-sm text-text-secondary mb-3">Review and edit the details before saving.</p>
<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" data-bind:_cafe-name required class="input-field" />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">City</span>
<input type="text" data-bind:_cafe-city class="input-field" />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Country</span>
<input type="text" data-bind:_cafe-country class="input-field" />
</label>
<label class="flex flex-col gap-1 text-sm">
<span class="text-text">Website</span>
<input
type="url"
data-bind:_cafe-website
class="input-field"
placeholder="https://&hellip;"
/>
</label>
</div>
<div class="mt-4 flex items-center justify-end gap-2">
<button
type="button"
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-text transition hover:bg-surface-alt"
data-on:click="$_reviewingCafe = false; $_cafeName = ''; $_cafeCity = ''; $_cafeCountry = ''; $_cafeLat = 0; $_cafeLng = 0; $_cafeWebsite = ''"
>
Back
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-accent-text transition hover:bg-accent-hover"
data-on:click="$_reviewingCafe = false; $_step = 2"
>
Next {{ icons::chevron_right("h-4 w-4") }}
</button>
</div>
</div>
<!-- Saved cafes -->
{% if !cafe_options.is_empty() %}
<div class="mt-3 border-t pt-3" data-show="!$_reviewingCafe">
<p class="mb-2 text-sm text-text-secondary">Or choose a saved cafe:</p>
<searchable-select
name="saved_cafe_id"
placeholder="Type to search saved cafes&hellip;"
data-on:change="$_cafeId = evt.detail.value; $_cafeName = evt.detail.display; $_cafeCity = evt.detail.data.city; $_cafeCountry = ''; $_cafeLat = 0; $_cafeLng = 0; $_cafeWebsite = ''; $_step = 2"
>
{% for cafe in cafe_options %}
<button
type="button"
value="{{ cafe.id }}"
data-display="{{ cafe.name }}"
data-city="{{ cafe.city }}"
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>
{% endif %}
</div>
</div>
<!-- Step 2: Roast identification -->
<div class="mt-4" data-show="$_step === 2" style="display: none">
<div class="rounded-lg border bg-surface p-5">
<h3 class="text-base font-semibold text-text mb-3">Select a coffee</h3>
<div data-show="!$_scanSuccess" class="mb-4">
<p class="text-sm text-text-secondary mb-3" data-show="!$_extracting">
Scan a bag to identify the coffee, or select from existing roasts below.
</p>
<form
id="checkin-scan-form"
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_roastName = $_scanSuccess; $_step = 3 } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Scan failed. Please try again.' }"
>
{{ scan::scan_input("checkin-scan-form", "checkin-image", "$_extracting",
"$_extractError", "Describe a coffee bag…", "Scanning…") }}
</form>
</div>
<div
class="mb-4 rounded-md bg-green-50 border border-green-200 px-3 py-2 text-sm text-green-800"
data-show="$_scanSuccess"
style="display: none"
>
Scanned: <span class="font-medium" data-text="$_scanSuccess"></span>
</div>
{% if !roast_options.is_empty() %}
<div class="border-t pt-3">
<p class="text-sm text-text-secondary mb-2">Or select an existing roast:</p>
<searchable-select
name="roast_id"
placeholder="Type to search existing roasts&hellip;"
data-on:change="$_roastId = evt.detail.value; $_roastName = evt.detail.display; $_roasterName = evt.detail.data.roaster; $_step = 3"
>
{% for roast in roast_options %}
<button
type="button"
value="{{ roast.id }}"
data-display="{{ roast.name }}"
data-roaster="{{ roast.roaster_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>
{% endif %}
</div>
</div>
<!-- Step 3: Review + submit -->
<div class="mt-4" data-show="$_step === 3" style="display: none">
<div class="rounded-lg border bg-surface p-5">
<h3 class="text-base font-semibold text-text mb-4">Review &amp; Submit</h3>
<dl class="flex flex-col gap-3 text-sm mb-6">
<div class="flex justify-between gap-2">
<dt class="font-medium text-text-muted">Cafe</dt>
<dd
class="text-right text-text"
data-text="$_cafeCity ? $_cafeName + ', ' + $_cafeCity : $_cafeName"
></dd>
</div>
<div class="flex justify-between gap-2">
<dt class="font-medium text-text-muted">Coffee</dt>
<dd
class="text-right text-text"
data-text="$_roasterName ? $_roastName + ' (' + $_roasterName + ')' : $_roastName"
></dd>
</div>
</dl>
<form
data-on:submit="$_submitting = true; $_error = ''; @post('/api/v1/check-in', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'error') { $_submitting = false; $_error = 'Check-in failed. Please try again.' }"
>
<input type="hidden" name="cafe_id" data-attr:value="$_cafeId" />
<input type="hidden" name="cafe_name" data-attr:value="$_cafeName" />
<input type="hidden" name="cafe_city" data-attr:value="$_cafeCity" />
<input type="hidden" name="cafe_country" data-attr:value="$_cafeCountry" />
<input type="hidden" name="cafe_lat" data-attr:value="$_cafeLat" />
<input type="hidden" name="cafe_lng" data-attr:value="$_cafeLng" />
<input type="hidden" name="cafe_website" data-attr:value="$_cafeWebsite" />
<input type="hidden" name="roast_id" data-attr:value="$_roastId" />
<button
type="submit"
class="inline-flex w-full items-center justify-center gap-2 rounded-md bg-accent px-4 py-3 text-sm font-semibold text-accent-text transition hover:bg-accent-hover disabled:opacity-50"
data-attr:disabled="$_submitting"
>
{{ icons::check("h-4 w-4") }} Check In
</button>
</form>
</div>
</div>
</section>
{% endblock %}