"use client"; import { createContext, useContext, type ReactNode } from "react"; import { useFormContext, useWatch, type Control, type FieldValues, } from "react-hook-form"; import { FormLabelMarkContext } from "@/components/ui/form"; import { Attribution } from "@/components/attribution"; import { FETCHED_MARK_TITLE, type FetchedMarks } from "@/lib/dive-conditions"; import { isNonEmptyFieldValue } from "@/lib/dive-form-fields"; interface AutofilledMarksValue { control: unknown; isAutofilled: (name: string) => boolean; fetched?: FetchedMarks; } const AutofilledMarksContext = createContext(null); const DOT = "ml-1.5 inline-block size-1.5 rounded-full bg-teal align-middle"; function AutofilledDot() { return ; } /** * The mark on a dive's field that a weather service filled in: the form's dot, * titled for what filled it. Unlike the form's own, it is read aloud, since on the * dive page no legend explains it. */ export function FetchedMark() { return ( (filled in from weather data) ); } // Whether a field still stands as the fetch wrote it once saved: an emptied field // does, since the API keeps a fill it is sent a blank for. const keepsFetched = (value: unknown, written: unknown) => value === written || !isNonEmptyFieldValue(value); // Watches the one field it marks, so the dot goes the moment the diver picks // another value - which is what saving it would do to the stored mark. function FetchedFormMark({ name, marks, written, }: { name: string; marks: AutofilledMarksValue; written: unknown; }) { const value = useWatch({ control: marks.control as Control, name }); if (keepsFetched(value, written)) return ; return marks.isAutofilled(name) ? : null; } function AutofilledMark({ name }: { name: string }) { const marks = useContext(AutofilledMarksContext); const { control } = useFormContext(); // A dialog opened from inside the dive form - a picker's "Add new..." - has a form // and labels of its own, and its fields' names can collide with the dive's. if (!marks || control !== marks.control) return null; if (marks.fetched && name in marks.fetched.values) { return ( ); } return marks.isAutofilled(name) ? : null; } /** * What the marks mean, said once above the form while any field carries one, so * no finger has to hover a dot to find out. Hidden from a screen reader along * with the dots it explains. */ export function AutofilledLegend() { return (

Filled in for you

); } function FetchedSourceLine({ control, fetched, }: { control: Control; fetched: FetchedMarks; }) { const names = Object.keys(fetched.values); const values = useWatch({ control, name: names }) as unknown[]; if ( !names.some((name, index) => keepsFetched(values[index], fetched.values[name]), ) ) { return null; } return (

); } /** * The weather service's credit, as visible text under a form's fields while any of * them would still hold the value it filled in once saved (`keepsFetched`) - the * licence asks for it beside the data wherever the data is shown. */ export function FetchedSource({ control, }: { control: Control; }) { const marks = useContext(AutofilledMarksContext); if (!marks?.fetched || marks.control !== control) return null; if (Object.keys(marks.fetched.values).length === 0) return null; return ( ); } const renderMark = (name: string) => ; /** * Puts a teal dot after the label of every field in `control`'s form that * `isAutofilled` says the form changed on its own and the diver has not since, * and after every field that would still hold the value `fetched` says a weather * service wrote into the stored dive once saved (`keepsFetched`). */ export function AutofilledMarks({ control, isAutofilled, fetched, children, }: { control: Control; isAutofilled: (name: string) => boolean; fetched?: FetchedMarks; children: ReactNode; }) { return ( {children} ); }