brewlog/templates/pages/checkin.html
Jon Seager 5820c162f6
fix: address static assets code review findings
- Remove dead CSS color property in .tab-mobile
- Fix design token violations (text-red-500, fallback colors)
- Add disconnectedCallback to chip-scroll, image-upload, searchable-select
- Replace innerHTML with safe DOM APIs in world-map screen reader table
- Add credentials: same-origin to WebAuthn fetch calls
- Move page-specific scripts (donut-chart, location) out of base.html
- Add client-side image resizing (1920px max dimension)
- Resize app-icon-512.png from 2048x2048 to 512x512
- Document Datastar unsafe-eval CSP requirement
- Add static asset serving tests (16 routes)
- Add e2e tests for world-map, donut-chart, chip-scroll presence
- Add cache-busting query params to all static asset URLs
2026-02-13 17:13:02 +00:00

430 lines
17 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
defer
src="/static/js/location.js?v={{ version_info.commit }}"
></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 -->
<nav
aria-label="Check-in progress"
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}"
data-attr:aria-current="$_step === 1 ? 'step' : null"
>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}"
data-attr:aria-current="$_step === 2 ? 'step' : null"
>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}"
data-attr:aria-current="$_step === 3 ? 'step' : null"
>Submit</span
>
</div>
</div>
</nav>
<!-- 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-error"
data-show="$_error"
style="display: none"
data-text="$_error"
role="alert"
></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
aria-required="true"
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>
<input type="hidden" name="cafe_image" id="checkin-cafe-image" />
<image-upload
mode="deferred"
target-input="checkin-cafe-image"
class="mt-4 flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-text-muted/30 bg-surface p-4 text-center text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"
/>
</svg>
<span class="text-xs">Add cafe photo (optional)</span>
</image-upload>
<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 = ''; document.getElementById('checkin-cafe-image').value = ''"
>
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-success-bg border border-success-border px-3 py-2 text-sm text-success-text"
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 = ''; document.getElementById('checkin-cafe-image-submit').value = document.getElementById('checkin-cafe-image').value || ''; @post('/api/v1/check-in', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_submitting) return; if (evt.detail.type === 'finished') { sessionStorage.setItem('toast', 'Checked in') } else 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" />
<input
type="hidden"
name="cafe_image"
id="checkin-cafe-image-submit"
/>
<input type="hidden" name="cup_image" id="checkin-cup-image" />
<image-upload
mode="deferred"
target-input="checkin-cup-image"
class="mb-4 flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-text-muted/30 bg-surface p-4 text-center text-text-muted cursor-pointer hover:border-accent/40 hover:text-text-secondary transition"
>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"
/>
</svg>
<span class="text-xs">Add cup photo (optional)</span>
</image-upload>
<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 %}