"use client"; /** * MobileWaterLogForm — measurement entry screen. * * Visual structure (Apple HIG form): * - Sticky top nav: back chevron + "Log Reading" title. * - Sticky headgate card under the nav — picks a tap target * bigger than the iOS minimum (44pt) and lets the user confirm * which headgate they're logging against at a glance. * - Section: MEASUREMENT * - HUGE numeric input (44pt font) — the visual focus of the * screen. `inputMode="decimal"` brings up the iOS numeric * keyboard with the decimal key. * - Segmented control for units (CFS / GPM / gal / ac-in / * ac-ft). The default selection is the headgate's configured * unit. * - Live "thresholds" hint below the input (e.g. "Normal range: * 2–8 CFS"). * - Section: TIMESTAMP (read-only, auto-updating every second so * the user can verify they're logging against "now"). * - Section: NOTES (optional, 500-char multiline). * - Sticky submit bar at the bottom — green primary button per * HIG success semantics. Slides up out of the keyboard's way * because the bar is `position: sticky; bottom: 0`. */ import { useEffect, useMemo, useRef, useState, type FormEvent, } from "react"; import { SegmentedControl } from "./SegmentedControl"; import type { FieldHeadgate, MobileUnit } from "./types"; import { MOBILE_UNITS } from "./types"; import { ChevronLeft, Clock, Gauge, Pencil, Pin, Spinner } from "./icons"; import { formatLongTimestamp, formatMeasurement, headgateSubtitle, } from "./format"; type Props = { headgate: FieldHeadgate; onSubmit: (input: { measurement: number; unit: string; notes: string }) => Promise; onChangeHeadgate: () => void; onLogout: () => void; /** Optional preset units — defaults to MOBILE_UNITS. */ units?: ReadonlyArray; }; const NOTES_MAX = 500; export function MobileWaterLogForm({ headgate, onSubmit, onChangeHeadgate, onLogout, units = MOBILE_UNITS, }: Props) { // ── State ──────────────────────────────────────────────────────── const [measurement, setMeasurement] = useState(""); const [unit, setUnit] = useState( units.includes(headgate.unit) ? headgate.unit : units[0], ); const [notes, setNotes] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [now, setNow] = useState(() => new Date()); const measurementRef = useRef(null); // ── Auto-focus the measurement input on mount so the keyboard // rises immediately. useEffect(() => { const t = window.setTimeout(() => measurementRef.current?.focus(), 80); return () => window.clearTimeout(t); }, []); // ── Live clock — re-renders every second. The setInterval is // cheap (one DOM update per second on a hidden element) and // gives the user confidence the timestamp reflects "now". useEffect(() => { const id = window.setInterval(() => setNow(new Date()), 1000); return () => window.clearInterval(id); }, []); // ── Threshold hint derived from the headgate. ──────────────────── const thresholdHint = useMemo(() => { const h = headgate.high_threshold; const l = headgate.low_threshold; if (h == null && l == null) return null; if (h != null && l != null) { return `Normal range: ${l} – ${h} ${unit}`; } if (h != null) return `Alert above ${h} ${unit}`; return `Alert below ${l} ${unit}`; }, [headgate, unit]); const submitted = (e?: FormEvent) => { e?.preventDefault(); if (submitting) return; const value = parseFloat(measurement); if (!Number.isFinite(value) || value < 0) { setError("Enter a measurement greater than or equal to 0"); return; } if (value > 1_000_000) { setError("That value is implausibly large"); return; } setSubmitting(true); setError(null); onSubmit({ measurement: value, unit, notes }) .catch((err) => { setError( err instanceof Error ? err.message : "Couldn't submit. Try again.", ); setSubmitting(false); }); // On success the parent swaps to the success screen. }; const subtitle = headgateSubtitle(headgate); const canSubmit = measurement.trim().length > 0 && Number.isFinite(parseFloat(measurement)) && parseFloat(measurement) >= 0 && !submitting; return (
{/* ── Top nav ────────────────────────────────────────────── */}

Log Reading

{/* ── Scrollable form body ───────────────────────────────── */}
{/* ── Headgate summary card (sticky) ──────────────── */} {/* ── Measurement section ─────────────────────────── */} Measurement
{ setMeasurement(e.target.value); if (error) setError(null); }} placeholder="0" aria-label="Measurement value" className="min-w-0 flex-1 appearance-none bg-transparent text-[44px] font-bold leading-[1.05] tracking-tight text-[#1d1d1f] outline-none placeholder:text-[rgba(60,60,67,0.25)]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif", // Hide the native spinner buttons — they crowd a // small mobile screen and feel non-native. MozAppearance: "textfield", }} /> {unit}
{thresholdHint && (

{thresholdHint}

)}
{/* Unit segmented control */}
setUnit(v)} options={units.map((u) => ({ value: u as string, label: u }))} />
{/* ── Timestamp section ───────────────────────────── */} Timestamp
Logged now
{formatLongTimestamp(now)}
{/* ── Notes section ────────────────────────────────── */} Notes — optional