- 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
121 lines
4.5 KiB
TypeScript
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;
|