import { useState, type KeyboardEvent } from 'react' import { Alert } from '@/components/ui/Alert' import { Button, IconButton } from '@/components/ui/Button' import { TextAreaField, TextField } from '@/components/ui/Field' import { errorMessage } from '@/lib/api' import { cn } from '@/lib/cn' import { today } from '@/lib/dates' import { formatObservedAt, useCreateJournalEntry, useDeleteJournalEntry, useJournal, useUpdateJournalEntry, type JournalEntry, type JournalFilter, } from '@/lib/journal' import type { EditorPlanting } from '@/lib/plantings' import { objectDisplayName } from './kinds' import { useEditorStore } from './store' import type { EditorObject } from './types' /** What a new note attaches to: the selection, else the focused bed, else the * garden — and how to say it. */ export interface JournalAttach { objectId?: number plantingId?: number label: string | null } /** * The grow journal: entries newest first as cards (what it's about, when, the * note), a one-line composer that attaches to whatever's selected — Enter * submits — and, when the rail was opened from a bed's "N notes", a filter chip * narrowing the list to that thing. Notes get written standing in the garden * holding a phone, so the composer is never more than a tap away. */ export function JournalTab({ gardenId, canEdit, currentUserId, isOwner, objects, plantings, attach, composerFirst = false, large = false, }: { gardenId: number canEdit: boolean currentUserId?: number isOwner: boolean objects: EditorObject[] plantings: EditorPlanting[] attach: JournalAttach /** Phone: the input above the entries (the thumb is at the bottom anyway). */ composerFirst?: boolean large?: boolean }) { const scope = useEditorStore((s) => s.journalScope) const setScope = useEditorStore((s) => s.setJournalScope) const filter: JournalFilter = scope?.type === 'object' ? { objectId: scope.id } : scope?.type === 'plop' ? { plantingId: scope.id } : {} const journal = useJournal(gardenId, filter) const entries = journal.data?.pages.flatMap((p) => p.entries) ?? [] const scopeName = scope?.type === 'object' ? objectDisplayName(objects.find((o) => o.id === scope.id) ?? { name: '', kind: 'object' }) : scope?.type === 'plop' ? 'one planting' : null const composer = canEdit && ( ) return (
{scopeName && (
Notes about {scopeName} setScope(null)} />
)} {composerFirst && composer} {journal.isPending &&

Loading…

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

{scopeName ? `Nothing written about ${scopeName} 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 && ( )} {!composerFirst &&
{composer}
}
) } function Composer({ gardenId, attach, large }: { gardenId: number; attach: JournalAttach; large: boolean }) { const create = useCreateJournalEntry(gardenId) const [body, setBody] = useState('') const [error, setError] = useState(null) const submit = () => { const text = body.trim() if (!text || create.isPending) return setError(null) create.mutate( { body: text, observedAt: today(), objectId: attach.objectId, plantingId: attach.plantingId }, { onSuccess: () => setBody(''), onError: (err) => setError(errorMessage(err, "Couldn't save that note.")) }, ) } const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() submit() } } return (
setBody(e.target.value)} onKeyDown={onKey} />
{error && {error}}
) } function Entry({ entry, gardenId, objects, plantings, canDelete, canRewrite, large, }: { entry: JournalEntry gardenId: number objects: EditorObject[] plantings: EditorPlanting[] canDelete: boolean canRewrite: boolean large: boolean }) { const update = useUpdateJournalEntry(gardenId) const del = useDeleteJournalEntry(gardenId) const [editing, setEditing] = useState(false) const [draft, setDraft] = useState(entry.body) const [observedAt, setObservedAt] = useState(entry.observedAt) const [error, setError] = useState(null) // A plop-level note names its bed; an object-level note names the object. const objectId = entry.objectId ?? (entry.plantingId != null ? plantings.find((p) => p.id === entry.plantingId)?.objectId : undefined) const about = objects.find((o) => o.id === objectId) const aboutLabel = about ? objectDisplayName(about) + (entry.plantingId != null ? ' · planting' : '') : entry.plantingId != null ? 'a planting' : 'Garden' const save = () => { const text = draft.trim() if (!text) return setError("An entry needs some text — delete it instead if that's what you meant.") setError(null) update.mutate( { id: entry.id, version: entry.version, body: text, observedAt }, { onSuccess: () => setEditing(false), onError: (err) => setError(errorMessage(err, "Couldn't save that edit.")) }, ) } return (
{aboutLabel} {formatObservedAt(entry.observedAt)}
{editing ? (
setDraft(e.target.value)} /> setObservedAt(e.target.value)} />
) : (
{entry.body}
)} {(canRewrite || canDelete) && !editing && (
{canRewrite && ( )} {canDelete && ( )}
)} {error &&

{error}

}
) }