"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { RepeatableRowField } from "@/components/ui/repeatable-row"; import { ABUNDANCES, ABUNDANCE_LABELS, ABUNDANCE_RANGES, abundanceOfCount, type SightingAbundance, type SightingWrite, } from "@/lib/api/dives"; import { cn } from "@/lib/utils"; /** How many were seen: a count, a band, or - neither key present - not counted. */ export type SightingQuantity = Pick; // A sighting with its quantity replaced whole, so a row never carries a count and a // band at once: the API refuses one that does. Neither is "seen, not counted", spelled // by leaving both keys off - never with a `0` or a `1`. export function withQuantity( sighting: SightingWrite, quantity: SightingQuantity, ): SightingWrite { const next = { ...sighting }; delete next.count; delete next.abundance; return { ...next, ...quantity }; } // What a count box takes: a positive whole number and nothing else. A keystroke that // would leave anything else in it is refused rather than committed, so a count is never // a `0` or a fraction waiting for the schema to object. const COUNT_TEXT = /^[1-9]\d*$/; interface QuickChoice { key: "one" | SightingAbundance; label: string; range: string; } const QUICK_CHOICES: QuickChoice[] = [ { key: "one", label: "One", range: "1" }, ...ABUNDANCES.map((band) => ({ key: band, label: ABUNDANCE_LABELS[band], range: ABUNDANCE_RANGES[band], })), ]; interface SightingQuantityFieldProps { id: string; // The species' display name, which names the box and the choices to a screen reader. speciesLabel: string; value: SightingQuantity; onChange: (quantity: SightingQuantity) => void; countError?: string; abundanceError?: string; disabled?: boolean; } /** * One question, how many: the count box, and one, few, many and abundant as quick * choices beside it. Each choice is a toggle, and every way in leaves the row carrying * one answer - a typed count clears the band, a chosen band clears the box, and * choosing the chosen one again takes the answer off. A typed count marks the band it * falls in without choosing it, so one tap turns "37" into "many". */ export function SightingQuantityField({ id, speciesLabel, value, onChange, countError, abundanceError, disabled, }: SightingQuantityFieldProps) { const countErrorId = `${id}-error`; const abundanceErrorId = `${id}-abundance-error`; const countsBand = value.count === undefined ? null : abundanceOfCount(value.count); const setCountText = (text: string) => { if (text === "") onChange({}); else if (COUNT_TEXT.test(text)) onChange({ count: Number(text) }); }; const choose = (key: QuickChoice["key"]) => { if (key === "one") onChange(value.count === 1 ? {} : { count: 1 }); else onChange(value.abundance === key ? {} : { abundance: key }); }; return ( {/* Text with a numeric keypad rather than `type="number"`, which lets "e", "-" and "1.5" into the box and reports them as an empty value - so a refused keystroke would read as a cleared count. */} setCountText(event.target.value)} />
{QUICK_CHOICES.map(({ key, label, range }) => { const chosen = key === "one" ? value.count === 1 : value.abundance === key; // The band a typed count falls in: shown, not chosen, so it is no // pressed state to assistive technology - choosing it stores the band. const counted = !chosen && countsBand === key; return ( ); })}
{countError && (

{countError}

)} {abundanceError && (

{abundanceError}

)}
); }