feat: garden containers crud (task 71)

- add listContainers/getContainer/searchContainers queries
- add createContainer/updateContainer/deleteContainer server actions with logActivity
- add ContainerList, ContainerDetail, ContainerForm client components
- add /garden, /garden/containers/[id], /garden/containers/new pages
- register garden.container entity with share + search in manifest
- add sprout icon to NavIcon registry
- add garden e2e test spec
This commit is contained in:
ginnoir
2026-06-01 19:28:34 -05:00
parent 2fd0677c5f
commit 5f6b756342
12 changed files with 756 additions and 4 deletions
+14
View File
@@ -0,0 +1,14 @@
import { notFound } from "next/navigation";
import { getContainer } from "@/modules/garden/server/queries";
import { ContainerDetail } from "@/modules/garden/components/container-detail";
export default async function ContainerPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const container = await getContainer(id);
if (!container) notFound();
return (
<div className="page-content">
<ContainerDetail container={container} />
</div>
);
}
+70
View File
@@ -0,0 +1,70 @@
import { redirect } from "next/navigation";
import { createContainer } from "@/modules/garden/server/actions";
export default function NewContainerPage() {
async function handleCreate(formData: FormData) {
"use server";
await createContainer({
name: formData.get("name") as string,
type: (formData.get("type") as string) || "other",
locationNotes: (formData.get("locationNotes") as string) || null,
});
redirect("/garden");
}
return (
<div className="page-content max-w-lg">
<h1 className="page-title">New container</h1>
<form action={handleCreate} className="flex flex-col gap-4 mt-4">
<div className="flex flex-col gap-1">
<label htmlFor="name" className="text-sm font-medium">
Name
</label>
<input id="name" name="name" required maxLength={120} className="input" />
</div>
<div className="flex flex-col gap-1">
<label htmlFor="type" className="text-sm font-medium">
Type
</label>
<select id="type" name="type" defaultValue="other">
{(
[
["shelf", "Shelf"],
["terrarium", "Terrarium"],
["raised-bed", "Raised bed"],
["window-box", "Window box"],
["single-pot", "Single pot"],
["outdoor", "Outdoor"],
["other", "Other"],
] as const
).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="locationNotes" className="text-sm font-medium">
Location notes
</label>
<textarea
id="locationNotes"
name="locationNotes"
maxLength={500}
rows={2}
className="input"
/>
</div>
<div className="flex gap-2 justify-end">
<a href="/garden" className="btn btn-ghost">
Cancel
</a>
<button type="submit" className="btn btn-primary">
Create
</button>
</div>
</form>
</div>
);
}