import { useState } from 'react' import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' import { TextArea } from '@/components/ui/TextArea' import { TextField } from '@/components/ui/TextField' import { errorMessage } from '@/lib/api' import { formatObservedAt, today, useCreateJournalEntry, useDeleteJournalEntry, useJournal, useUpdateJournalEntry, type JournalEntry, } from '@/lib/journal' import type { EditorObject } from './types' import { objectDisplayName } from './kinds' // Shared styling for the small From/To date inputs, so the two stay in step and // don't drift from each other. const dateInputClass = 'rounded-md border border-border bg-surface px-1.5 py-1 text-fg outline-none focus-visible:ring-2 focus-visible:ring-accent/40' /** * The garden's journal: write an entry, read the season back. * * Notes get written standing in the garden holding a phone, usually one-handed. * If it takes more than a couple of taps from looking at a bed to typing a * sentence, the log stays empty — so the composer is open by default rather than * behind an "add" button, and selecting a bed pre-scopes it to that bed. */ export function JournalPanel({ gardenId, canEdit, currentUserId, isOwner, objects, scopeObjectId, onScopeChange, scopePlantingId, onScopePlantingChange, }: { gardenId: number canEdit: boolean currentUserId?: number isOwner: boolean objects: EditorObject[] /** Which bed the panel is filtered to, if any. */ scopeObjectId: number | null onScopeChange: (id: number | null) => void /** Which single plop the panel is filtered to, if any (#85). The store keeps * this mutually exclusive with scopeObjectId. Required like its bed twin. */ scopePlantingId: number | null onScopePlantingChange: (id: number | null) => void }) { // Date-range narrowing (#85): the backend and JournalFilter already supported // from/to; they just had no UI. Empty inputs don't filter. const [from, setFrom] = useState('') const [to, setTo] = useState('') const scopedObject = objects.find((o) => o.id === scopeObjectId) ?? null // One source of scope priority — plop over bed — for both the filter and the // composer's label, so they can't drift. const scopeLabel = scopePlantingId != null ? 'this planting' : scopedObject ? objectDisplayName(scopedObject) : null const filter = { ...(scopePlantingId != null ? { plantingId: scopePlantingId } : scopeObjectId != null ? { objectId: scopeObjectId } : {}), ...(from ? { from } : {}), ...(to ? { to } : {}), } const journal = useJournal(gardenId, filter) const entries = journal.data?.pages.flatMap((p) => p.entries) ?? [] return (

Journal

{objects.length > 0 && ( )}
{scopePlantingId != null && (
Notes about one planting
)}
{(from || to) && ( )}
{canEdit && ( )} {journal.isPending &&

Loading…

} {journal.isError && entries.length === 0 && ( {errorMessage(journal.error, 'Could not load the journal.')} )} {journal.isSuccess && entries.length === 0 && (

{scopePlantingId != null ? 'Nothing written about this planting yet.' : scopedObject ? `Nothing written about ${objectDisplayName(scopedObject)} yet.` : 'Nothing written yet. This is where what happened goes — when something went in, what came up, what the frost got. Next year you get to read it back.'}

)}
    {entries.map((e) => ( ))}
{journal.hasNextPage && ( )} {journal.isError && entries.length > 0 && (

{errorMessage(journal.error, "Couldn't load older entries.")}

)}
) } function Composer({ gardenId, objectId, plantingId = null, scopeLabel, }: { gardenId: number objectId: number | null /** When set, the note attaches to this plop rather than a bed (#85). */ plantingId?: number | null scopeLabel: string | null }) { const create = useCreateJournalEntry(gardenId) const [body, setBody] = useState('') // Editable so an observation can be backdated: you write up Saturday on Sunday. const [observedAt, setObservedAt] = useState(today()) const [error, setError] = useState(null) const submit = () => { const text = body.trim() if (!text) return setError(null) create.mutate( { body: text, observedAt, objectId: objectId ?? undefined, plantingId: plantingId ?? undefined }, { onSuccess: () => { setBody('') setObservedAt(today()) }, onError: (err) => setError(errorMessage(err, "Couldn't save that note.")), }, ) } return (