import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' import { errorMessage } from '@/lib/api' import { describeCounts, totalChanges, useGardenHistory, useUndo, type ChangeSet } from '@/lib/history' import { cn } from '@/lib/cn' import { UndoButton } from './UndoButton' /** * The garden's change history, newest first, with an undo on each entry. * * A reverted entry stays in the list, marked — and the revert appears as its own * entry, because that is what it is. Making the original disappear would be * rewriting history rather than adding to it, and would leave no way to undo the * undo. */ export function HistoryPanel({ gardenId, canEdit }: { gardenId: number; canEdit: boolean }) { const history = useGardenHistory(gardenId) const undo = useUndo(gardenId) const sets = history.data?.pages.flatMap((p) => p.changeSets) ?? [] return (

History

{history.isPending &&

Loading…

} {history.isError && sets.length === 0 && ( {errorMessage(history.error, 'Could not load history.')} )} {history.isSuccess && sets.length === 0 && (

Nothing yet. Every change you make here shows up in this list, and can be undone from it.

)}
    {sets.map((cs) => ( ))}
{history.hasNextPage && ( <> {/* isError stays true for a failed page even though earlier pages loaded, so say so rather than leaving a button that did nothing. */} {history.isError && sets.length > 0 && (

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

)} )} {/* Said plainly rather than discovered: deleting a garden bypasses this list entirely, because the delete cascades below the layer that records changes. Better to state the gap than to imply cover we don't have. */}

Deleting a whole garden isn't covered here and can't be undone.

) } function HistoryEntry({ changeSet, canEdit, undo, }: { changeSet: ChangeSet canEdit: boolean undo: ReturnType }) { const counts = describeCounts(changeSet) const reverted = changeSet.revertedById != null const isRevert = changeSet.revertsId != null return (
  • {changeSet.summary}

    {changeSet.source === 'agent' && ( agent )} {isRevert && undo} {changeSet.actorName} · {counts && ( <> · {counts} )}

    {reverted &&

    Undone

    }
    {canEdit && totalChanges(changeSet) > 0 && ( )}
  • ) } /** A compact "3m ago" / "yesterday" for a UTC timestamp. */ export function relativeTime(iso: string): string { const then = Date.parse(iso) if (Number.isNaN(then)) return iso const seconds = (Date.now() - then) / 1000 // Clock skew between the server and this browser can put a just-written entry // slightly in the future; that's "just now", not a negative duration. if (seconds < 60) return 'just now' // Floor throughout: 18 hours ago is "18h ago", not "yesterday". Rounding up // into the next unit reads as a bigger gap than actually elapsed. const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) if (days === 1) return 'yesterday' if (days < 30) return `${days}d ago` return new Date(then).toLocaleDateString() }