+ {d.coverImageUrl && (
+

+ )}
{d.name}
{d.type}
{d.locationNotes &&
{d.locationNotes}
}
+ {d.images.length > 1 && (
+
+ {d.images.slice(1).map((url) => (
+

+ ))}
+
+ )}
{d.plants.length > 0 && (
Plants
diff --git a/src/modules/garden/schema.ts b/src/modules/garden/schema.ts
index 1a42210..a81a220 100644
--- a/src/modules/garden/schema.ts
+++ b/src/modules/garden/schema.ts
@@ -23,6 +23,7 @@ export const gardenContainers = pgTable(
type: text("type").notNull().default("other"),
locationNotes: text("location_notes"),
coverImageUrl: text("cover_image_url"),
+ images: jsonb("images").notNull().default([]).$type
(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
diff --git a/src/modules/garden/server/actions.ts b/src/modules/garden/server/actions.ts
index da1329f..233dad7 100644
--- a/src/modules/garden/server/actions.ts
+++ b/src/modules/garden/server/actions.ts
@@ -93,6 +93,80 @@ export async function deleteContainer(input: { id: string }) {
revalidatePath("/garden");
}
+export async function addContainerImage(input: { id: string; url: string }) {
+ const parsed = z.object({ id: z.string().uuid(), url: z.string().min(1) }).parse(input);
+ const { household } = await getCurrentSession();
+ await assertCanAccessContainer(parsed.id, household.id);
+
+ const [row] = await db
+ .select({ images: gardenContainers.images, coverImageUrl: gardenContainers.coverImageUrl })
+ .from(gardenContainers)
+ .where(eq(gardenContainers.id, parsed.id))
+ .limit(1);
+
+ if (!row) throw new Error("Container not found");
+ if (row.images.length >= 10) throw new Error("Maximum 10 images allowed");
+
+ const newImages = [...row.images, parsed.url];
+ await db
+ .update(gardenContainers)
+ .set({
+ images: newImages,
+ coverImageUrl: row.images.length === 0 ? parsed.url : row.coverImageUrl,
+ updatedAt: new Date(),
+ })
+ .where(eq(gardenContainers.id, parsed.id));
+
+ revalidatePath(`/garden/containers/${parsed.id}`);
+}
+
+export async function removeContainerImage(input: { id: string; url: string }) {
+ const parsed = z.object({ id: z.string().uuid(), url: z.string() }).parse(input);
+ const { household } = await getCurrentSession();
+ await assertCanAccessContainer(parsed.id, household.id);
+
+ const [row] = await db
+ .select({ images: gardenContainers.images, coverImageUrl: gardenContainers.coverImageUrl })
+ .from(gardenContainers)
+ .where(eq(gardenContainers.id, parsed.id))
+ .limit(1);
+
+ if (!row) throw new Error("Container not found");
+
+ const newImages = row.images.filter((u) => u !== parsed.url);
+ const wasPrimary = row.coverImageUrl === parsed.url;
+ const newCover = wasPrimary ? (newImages[0] ?? null) : row.coverImageUrl;
+
+ await db
+ .update(gardenContainers)
+ .set({ images: newImages, coverImageUrl: newCover, updatedAt: new Date() })
+ .where(eq(gardenContainers.id, parsed.id));
+
+ revalidatePath(`/garden/containers/${parsed.id}`);
+}
+
+export async function setContainerPrimaryImage(input: { id: string; url: string }) {
+ const parsed = z.object({ id: z.string().uuid(), url: z.string() }).parse(input);
+ const { household } = await getCurrentSession();
+ await assertCanAccessContainer(parsed.id, household.id);
+
+ const [row] = await db
+ .select({ images: gardenContainers.images })
+ .from(gardenContainers)
+ .where(eq(gardenContainers.id, parsed.id))
+ .limit(1);
+
+ if (!row) throw new Error("Container not found");
+ if (!row.images.includes(parsed.url)) throw new Error("Image not in container gallery");
+
+ await db
+ .update(gardenContainers)
+ .set({ coverImageUrl: parsed.url, updatedAt: new Date() })
+ .where(eq(gardenContainers.id, parsed.id));
+
+ revalidatePath(`/garden/containers/${parsed.id}`);
+}
+
async function assertCanAccessContainer(id: string, householdId: string) {
const [row] = await db
.select({ id: gardenContainers.id })
diff --git a/src/modules/garden/server/queries.ts b/src/modules/garden/server/queries.ts
index 9c18e60..65eabfd 100644
--- a/src/modules/garden/server/queries.ts
+++ b/src/modules/garden/server/queries.ts
@@ -10,6 +10,7 @@ export type ContainerDto = {
type: string;
locationNotes: string | null;
coverImageUrl: string | null;
+ images: string[];
plantCount: number;
createdAt: string;
updatedAt: string;
@@ -39,6 +40,7 @@ export async function listContainers(): Promise {
type: gardenContainers.type,
locationNotes: gardenContainers.locationNotes,
coverImageUrl: gardenContainers.coverImageUrl,
+ images: gardenContainers.images,
createdAt: gardenContainers.createdAt,
updatedAt: gardenContainers.updatedAt,
plantCount: sql`count(${gardenPlants.id})::int`,
@@ -51,6 +53,7 @@ export async function listContainers(): Promise {
return rows.map((r) => ({
...r,
+ images: r.images ?? [],
plantCount: r.plantCount ?? 0,
createdAt: r.createdAt.toISOString(),
updatedAt: r.updatedAt.toISOString(),
@@ -68,6 +71,7 @@ export async function getContainer(id: string): Promise`(select count(*)::int from garden_plants where container_id = ${gardenContainers.id})`,
@@ -98,6 +102,7 @@ export async function getContainer(id: string): Promise