Files
growstuff/app/javascript/components/GardenCards.jsx
T
Brenda WallaceandClaude Sonnet 5 a3dfbe5016 Edit a planting in a dialog over the garden list
Three dots > Edit opens the planting's form in a modal instead of going to
the edit page. It saves as JSON and swaps in the refreshed garden card, so
the page is never left. Ctrl/cmd-click still opens the edit page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:42:26 +12:00

92 lines
3.5 KiB
React

import React, {useEffect, useRef, useState} from 'react';
import EditPlantingModal from './EditPlantingModal';
import GardenCard from './GardenCard';
import PlantSomethingModal from './PlantSomethingModal';
const plantingIds = (garden) => [...garden.perennials, ...garden.annuals].map((planting) => planting.id);
// The list of garden cards on the gardens index. Props come from
// GardensHelper#garden_cards_props. It owns the cards' state so that saving a
// planting from the "Plant something here" dialog can swap in the garden's
// updated card, and the new planting appears without a page reload.
export default function GardenCards({gardens: initialGardens, default_icon_url: defaultIconUrl, spade_icon_url: spadeIconUrl}) {
const [gardens, setGardens] = useState(initialGardens);
const [plantingIn, setPlantingIn] = useState(null); // the garden the dialog is open for
const [editing, setEditing] = useState(null); // the planting the edit dialog is open for
const [notice, setNotice] = useState(null);
const [highlightedId, setHighlightedId] = useState(null);
const highlightTimer = useRef(null);
// The "jump to" links point at #garden-<id>, but the cards only exist once
// React has mounted, after the browser has already tried to scroll.
useEffect(() => {
if (!window.location.hash) return;
const target = document.getElementsByName(window.location.hash.slice(1))[0];
if (target) target.scrollIntoView();
}, []);
useEffect(() => () => clearTimeout(highlightTimer.current), []);
// The response leaves out the owner (the page already has it); keep ours.
function replaceCard(updatedCard) {
setGardens((current) => current.map((garden) => (garden.id === updatedCard.id ? {...updatedCard, owner: garden.owner} : garden)));
}
function created(updatedCard, crop) {
const before = gardens.find((garden) => garden.id === updatedCard.id);
const known = new Set(plantingIds(before));
const added = plantingIds(updatedCard).find((id) => !known.has(id));
replaceCard(updatedCard);
setPlantingIn(null);
setNotice(`Planted ${crop ? crop.name : 'something'} in ${updatedCard.name}.`);
setHighlightedId(added);
clearTimeout(highlightTimer.current);
highlightTimer.current = setTimeout(() => setHighlightedId(null), 4000);
}
function edited(updatedCard, planting) {
replaceCard(updatedCard);
setEditing(null);
setNotice(`Saved changes to ${planting.crop.name}.`);
}
return (
<>
{notice && (
<div className="alert alert-success alert-dismissible" role="status">
<i className="fa fa-check-circle" aria-hidden="true" /> {notice}
<button type="button" className="btn-close" aria-label="Dismiss" onClick={() => setNotice(null)} />
</div>
)}
{gardens.map((garden) => (
<GardenCard
key={garden.id}
garden={garden}
defaultIconUrl={defaultIconUrl}
onPlant={setPlantingIn}
highlightedId={highlightedId}
onPlantingUpdated={replaceCard}
onEditPlanting={setEditing}
/>
))}
{editing && (
<EditPlantingModal
planting={editing}
onClose={() => setEditing(null)}
onSaved={(updatedCard) => edited(updatedCard, editing)}
/>
)}
{plantingIn && (
<PlantSomethingModal
garden={plantingIn}
iconUrl={spadeIconUrl}
onClose={() => setPlantingIn(null)}
onCreated={created}
/>
)}
</>
);
}