import React, {useEffect, useState} from 'react'; import {getJson, postJson} from '../api'; import {formatDate} from '../dates'; import Modal from './Modal'; import PlantPartPicker from './PlantPartPicker'; import Steps from './Steps'; const STEPS = ['Choose a part', 'How much?', 'When?', 'Any notes?']; const WHEN_CHOICES = [['today', 'Today'], ['yesterday', 'Yesterday'], ['other', 'Enter date']]; function humanize(field) { return field.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase()); } function errorMessages(status, data) { if (status === 422 && data && data.errors) { const messages = Object.entries(data.errors).flatMap(([field, list]) => list.map((m) => `${humanize(field)} ${m}`)); if (messages.length > 0) return messages; } if (status === 401) return ['Please sign in again to record this harvest.']; return ['Something went wrong saving that. Please try again.']; } // "2026-09-21" a number of days earlier (or later, if negative), as // "2026-09-20". Worked out in UTC so the browser's time zone can't shift it. function daysBefore(iso, days) { const [year, month, day] = iso.split('-').map(Number); return new Date(Date.UTC(year, month - 1, day - days)).toISOString().slice(0, 10); } // Recording a harvest from a garden card, as a dialog over the list, in steps // like the Add planting dialog: which part of the plant you harvested, how // much, when (today unless you say otherwise), then any notes, and save. The // crop is the planting's. // It loads that, and the choices, from /plantings/:slug/harvests/new.json, saves // with POST /harvests.json, and hands the refreshed garden card to onSaved, so // the planting's badges follow and the page is never left. export default function RecordHarvestModal({planting, iconUrl, onClose, onSaved}) { const [form, setForm] = useState(null); // {planting_id, plant_parts, plant_part_id (the usual one), units, weight_units, ...} const [values, setValues] = useState(null); const [part, setPart] = useState(null); // step 1's answer const [step, setStep] = useState(1); const [when, setWhen] = useState('today'); // today, yesterday, or other (a date you enter) const [loadError, setLoadError] = useState(false); const [saving, setSaving] = useState(false); const [errors, setErrors] = useState([]); useEffect(() => { const controller = new AbortController(); (async () => { try { const {ok, data} = await getJson(`${planting.url}/harvests/new.json`, {signal: controller.signal}); if (!ok) throw new Error('load failed'); setForm(data); setValues({ custom_date: '', quantity: '', unit: data.units[0].value, weight_quantity: '', weight_unit: data.weight_units[0].value, description: '', }); } catch (error) { if (error.name !== 'AbortError') setLoadError(true); } })(); return () => controller.abort(); }, [planting.url]); const set = (field) => (event) => setValues((current) => ({...current, [field]: event.target.value})); // The server's "today" is the one that counts, not the browser's. const today = form && form.harvested_at; const harvestedAt = {today, yesterday: today && daysBefore(today, 1), other: values && values.custom_date}[when]; // A harvest has to come after the planting (the server checks), so there is // nothing to offer before that: no Yesterday for a planting made yesterday // or today, and the date box starts the day after it was planted. const plantedOn = planting.planted_at; const choices = WHEN_CHOICES.filter(([choice]) => choice !== 'yesterday' || !plantedOn || (today && daysBefore(today, 1) > plantedOn)); const earliest = plantedOn ? daysBefore(plantedOn, -1) : undefined; function choosePart(chosen) { setPart(chosen); setStep(2); } function chooseWhen(choice) { setWhen(choice); if (choice === 'other' && !values.custom_date) setValues((current) => ({...current, custom_date: today})); } function amount() { const unit = form.units.find((candidate) => candidate.value === values.unit); const parts = [ values.quantity && `${values.quantity} ${unit ? unit.label : values.unit}`, values.weight_quantity && `${values.weight_quantity} ${values.weight_unit}`, ].filter(Boolean); return parts.length > 0 ? parts.join(' · ') : 'Not entered'; } // "Today, 21 Sep", or just the date when you entered one. function whenLabel() { const date = formatDate(harvestedAt); return when === 'other' ? date : `${WHEN_CHOICES.find(([value]) => value === when)[1]}, ${date}`; } // Each step's button goes on to the next; the last one saves. async function save(event) { event.preventDefault(); if (step < STEPS.length) { setStep(step + 1); return; } setSaving(true); setErrors([]); try { const {custom_date: customDate, ...fields} = values; const {ok, status, data} = await postJson('/harvests.json', { harvest: {planting_id: form.planting_id, plant_part_id: part.id, harvested_at: harvestedAt, ...fields}, }); if (ok) { onSaved(data.garden); // closes this dialog return; } setErrors(errorMessages(status, data)); } catch (error) { setErrors(['Couldn\'t reach the server. Please try again.']); } setSaving(false); } const title = ( <> {iconUrl && } Record a harvest of {planting.crop.name} ); return ( {loadError && (
Couldn’t load the harvest form. Please close this and try again.
)} {!loadError && !values && (