v0.1.1 — Garden: edit and delete plants

This commit is contained in:
Bonna Moon
2026-06-15 16:58:03 -05:00
parent eb43ec37a0
commit 78728963f4
11 changed files with 9650 additions and 3 deletions

View File

@@ -0,0 +1,64 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Trash2 } from "lucide-react";
interface Props {
plantId: string;
plantName: string;
}
export function DeletePlantButton({ plantId, plantName }: Props) {
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const router = useRouter();
const { toast } = useToast();
async function onDelete() {
setBusy(true);
const res = await fetch(`/api/plants/${plantId}`, { method: "DELETE" });
setBusy(false);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
toast({ title: "Error", description: err.error ?? "Could not delete plant.", variant: "destructive" });
return;
}
toast({ title: `${plantName} removed.` });
setOpen(false);
router.push("/garden");
router.refresh();
}
return (
<>
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive" onClick={() => setOpen(true)}>
<Trash2 className="h-4 w-4 mr-1" />
Delete
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Remove {plantName}?</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
This will remove the plant and all its log entries from your registry.
You can always add it back later.
</p>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={onDelete} disabled={busy}>
{busy ? "Removing…" : "Yes, remove it"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,157 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Plant, PlantCategory } from "@prisma/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { Pencil } from "lucide-react";
import { CATEGORY_LABELS, CATEGORY_ORDER } from "@/lib/garden/categories";
const schema = z.object({
commonName: z.string().min(1, "Name is required"),
species: z.string().optional(),
variety: z.string().optional(),
category: z.nativeEnum(PlantCategory),
zone: z.string().optional(),
plantedAt: z.string().optional(),
source: z.string().optional(),
notes: z.string().optional(),
});
type FormValues = z.infer<typeof schema>;
function toDateInput(d: Date | null): string {
if (!d) return "";
return new Date(d).toISOString().split("T")[0];
}
export function EditPlantButton({ plant }: { plant: Plant }) {
const [open, setOpen] = useState(false);
const router = useRouter();
const { toast } = useToast();
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
commonName: plant.commonName,
species: plant.species ?? "",
variety: plant.variety ?? "",
category: plant.category,
zone: plant.zone ?? "",
plantedAt: toDateInput(plant.plantedAt),
source: plant.source ?? "",
notes: plant.notes ?? "",
},
});
async function onSubmit(values: FormValues) {
const res = await fetch(`/api/plants/${plant.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
toast({ title: "Error", description: err.error ?? "Could not save changes.", variant: "destructive" });
return;
}
toast({ title: "Saved!" });
setOpen(false);
router.refresh();
}
return (
<>
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
<Pencil className="h-4 w-4 mr-1" />
Edit
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit plant</DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2 space-y-1.5">
<Label htmlFor="edit-commonName">Common name *</Label>
<Input id="edit-commonName" {...form.register("commonName")} />
{form.formState.errors.commonName && (
<p className="text-xs text-destructive">{form.formState.errors.commonName.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="edit-variety">Variety / cultivar</Label>
<Input id="edit-variety" {...form.register("variety")} />
</div>
<div className="space-y-1.5">
<Label>Category *</Label>
<Select
defaultValue={plant.category}
onValueChange={(v) => form.setValue("category", v as PlantCategory)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{CATEGORY_ORDER.map((cat) => (
<SelectItem key={cat} value={cat}>{CATEGORY_LABELS[cat]}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="col-span-2 space-y-1.5">
<Label htmlFor="edit-species">Scientific name</Label>
<Input id="edit-species" className="italic" {...form.register("species")} />
</div>
<div className="col-span-2 space-y-1.5">
<Label htmlFor="edit-zone">Location on your property</Label>
<Input id="edit-zone" {...form.register("zone")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="edit-plantedAt">Date planted</Label>
<Input id="edit-plantedAt" type="date" {...form.register("plantedAt")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="edit-source">Where it came from</Label>
<Input id="edit-source" {...form.register("source")} />
</div>
<div className="col-span-2 space-y-1.5">
<Label htmlFor="edit-notes">Notes</Label>
<Textarea id="edit-notes" rows={3} {...form.register("notes")} />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Saving…" : "Save changes"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}