Files
famapp/src/modules/garden/schema.ts
T
ginnoir c6a34f7471 feat: share links visible on plants/containers; container image gallery
- Fix ShareButton silently swallowing errors from createShareLink; now
  shows inline error text so failures are visible to the user
- Add getShareLinksForEntity server action and EntityShareLink type to
  _core/share.ts
- Add ShareLinkList component — renders active share links per entity
  with per-row Revoke; renders nothing when empty
- Wire ShareLinkList into plant and container detail pages (loaded
  server-side in parallel with the entity fetch)
- Add images jsonb column to garden_containers schema + migration 0018
- Add addContainerImage / removeContainerImage / setContainerPrimaryImage
  server actions mirroring the plant image pattern (10-image cap, first
  upload auto-sets cover)
- Update ContainerDetailDto, listContainers, getContainer to include images
- Rewrite ContainerDetail with Info/Gallery tabs; Gallery tab mirrors
  plant gallery (3-col grid, star/X overlays, upload button, counter)
- Update ContainerShareData and container renderSharedView to show cover
  image hero and secondary image grid on public share pages
2026-06-02 20:15:29 -05:00

121 lines
4.5 KiB
TypeScript

import {
boolean,
date,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { households, users } from "../_core/schema";
export const gardenContainers = pgTable(
"garden_containers",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
name: text("name").notNull(),
type: text("type").notNull().default("other"),
locationNotes: text("location_notes"),
coverImageUrl: text("cover_image_url"),
images: jsonb("images").notNull().default([]).$type<string[]>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index("garden_containers_household_idx").on(t.householdId)],
);
export const gardenPlants = pgTable(
"garden_plants",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
containerId: uuid("container_id").references(() => gardenContainers.id, {
onDelete: "set null",
}),
name: text("name").notNull(),
scientificName: text("scientific_name"),
speciesId: text("species_id"),
category: text("category").notNull().default("other"),
notes: text("notes"),
acquisitionDate: date("acquisition_date"),
growthStage: text("growth_stage"),
healthStatus: text("health_status").notNull().default("healthy"),
sunlight: text("sunlight"),
wateringNotes: text("watering_notes"),
fertilizingNotes: text("fertilizing_notes"),
primaryImageUrl: text("primary_image_url"),
images: jsonb("images").notNull().default([]).$type<string[]>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("garden_plants_household_idx").on(t.householdId),
index("garden_plants_household_container_idx").on(t.householdId, t.containerId),
],
);
export const gardenCareLogs = pgTable(
"garden_care_logs",
{
id: uuid("id").primaryKey().defaultRandom(),
plantId: uuid("plant_id")
.notNull()
.references(() => gardenPlants.id, { onDelete: "cascade" }),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
careType: text("care_type").notNull(),
performedBy: uuid("performed_by").references(() => users.id, { onDelete: "set null" }),
notes: text("notes"),
performedAt: timestamp("performed_at", { withTimezone: true }).notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("garden_care_logs_plant_performed_idx").on(t.plantId, t.performedAt),
index("garden_care_logs_household_idx").on(t.householdId),
],
);
export const gardenCareSchedules = pgTable(
"garden_care_schedules",
{
id: uuid("id").primaryKey().defaultRandom(),
plantId: uuid("plant_id")
.notNull()
.references(() => gardenPlants.id, { onDelete: "cascade" }),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
careType: text("care_type").notNull(),
intervalDays: integer("interval_days").notNull(),
lastPerformedAt: timestamp("last_performed_at", { withTimezone: true }),
nextDueAt: timestamp("next_due_at", { withTimezone: true }),
enabled: boolean("enabled").notNull().default(true),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex("garden_care_schedules_plant_type_uq").on(t.plantId, t.careType),
index("garden_care_schedules_household_due_idx").on(t.householdId, t.nextDueAt),
],
);
export const gardenSpeciesCache = pgTable("garden_species_cache", {
speciesId: text("species_id").primaryKey(),
data: jsonb("data").notNull().$type<Record<string, unknown>>(),
cachedAt: timestamp("cached_at", { withTimezone: true }).notNull().defaultNow(),
});
export type GardenContainer = typeof gardenContainers.$inferSelect;
export type GardenPlant = typeof gardenPlants.$inferSelect;
export type GardenCareLog = typeof gardenCareLogs.$inferSelect;
export type GardenCareSchedule = typeof gardenCareSchedules.$inferSelect;