- Add MinIO service to compose.yaml with named volume - Add generic /api/uploads POST+GET route with auth, 5 MB, image/* guards - Add src/lib/minio.ts singleton client with ensureBucket helper - Add garden Drizzle schema: containers, plants, care_logs, care_schedules, species_cache - Register garden module in src/modules/index.ts and src/lib/db.ts - Apply migration 0015_garden_schema.sql - Add MINIO_* and PERENUAL_API_KEY to .env.example - Add task briefs 70-75 for the full garden feature arc - Add uploads.spec.ts E2E test (RED → GREEN on route auth guard)
118 lines
5.3 KiB
Markdown
118 lines
5.3 KiB
Markdown
# 72 — Garden plants
|
|
|
|
## Goal
|
|
|
|
Implement full CRUD for individual **plants**. Each plant has a name, category, care notes, health status, up to 10 photos, an optional container assignment, and species data pulled from the **Perenual API** when the user searches for a known species.
|
|
|
|
## Depends on
|
|
|
|
- 70 (schema, MinIO, upload route), 71 (containers — container assignment picker)
|
|
|
|
## Scope
|
|
|
|
### Perenual API wrapper (`src/modules/garden/server/species-lookup.ts`)
|
|
|
|
Perenual (perenual.com) provides a free-tier plant species API. Key endpoint: `GET /api/species-list?key=<KEY>&q=<query>`.
|
|
|
|
Implement:
|
|
|
|
```typescript
|
|
export type SpeciesSuggestion = {
|
|
id: string;
|
|
common_name: string;
|
|
scientific_name: string;
|
|
watering: string; // e.g. "frequent", "average", "minimum"
|
|
sunlight: string[]; // e.g. ["full sun", "part shade"]
|
|
cycle: string; // e.g. "Perennial"
|
|
default_image_url: string | null;
|
|
};
|
|
|
|
export async function searchSpecies(query: string): Promise<SpeciesSuggestion[]>;
|
|
export async function getSpeciesById(id: string): Promise<SpeciesSuggestion | null>;
|
|
```
|
|
|
|
Cache results for 24 hours in a `garden_species_cache` table (columns: `species_id` text pk, `data` jsonb, `cached_at` timestamptz). Add this table to the schema in `src/modules/garden/schema.ts` and include it in the migration from task 70 (or add a new migration).
|
|
|
|
If the API is unreachable or returns an error, log the failure and return `[]` / `null`. `PERENUAL_API_KEY` from env; if absent, `searchSpecies` returns `[]` silently so the app works without the key.
|
|
|
|
### Server (`src/modules/garden/server/`)
|
|
|
|
Add to `actions.ts`:
|
|
|
|
- `createPlant(input)` — Zod-validated, `getCurrentSession`, insert, `logActivity`, `revalidatePath("/garden")`.
|
|
- `updatePlant(input: { id, ...partials })` — patch, household check, logActivity.
|
|
- `deletePlant(input: { id })` — household check, logActivity; care logs + schedules cascade-delete via DB.
|
|
- `addPlantImage(input: { id, url })` — appends URL to `images` jsonb array; reject if already 10 images.
|
|
- `removePlantImage(input: { id, url })` — removes URL from array; if it was `primary_image_url`, set primary to first remaining or null.
|
|
- `setPrimaryImage(input: { id, url })` — sets `primary_image_url`; url must already be in `images`.
|
|
|
|
Add to `queries.ts`:
|
|
|
|
- `listPlants({ containerId? })` — all plants in household optionally filtered by container. Include last-care timestamps per type via a lateral subquery.
|
|
- `getPlant(id)` — full plant data + container name + care log summary (last 5 entries) + active schedules.
|
|
|
|
### UI (`src/modules/garden/components/`)
|
|
|
|
**`plant-list.tsx`** (server component)
|
|
|
|
- Grouped by container first; "Unassigned" group for plants with no container.
|
|
- Each card: primary image thumbnail, name, health badge, "last watered X days ago", overdue care indicator.
|
|
- Replaces the placeholder in the "Plants" tab on `/garden/page.tsx` from task 71.
|
|
|
|
**`plant-detail.tsx`** (server component)
|
|
|
|
- Three-tab layout: **Info**, **Gallery**, **Care** (Care tab filled in by task 73).
|
|
- Info tab: all fields, container link, health/stage badges, species info.
|
|
- Gallery tab: photo grid, primary image star-toggle, delete individual image, upload new image.
|
|
|
|
**`plant-form.tsx`** (client component)
|
|
|
|
- Fields: name (required), category (select), container (select, nullable), health status (select), growth stage (select).
|
|
- Species search combobox: debounced search → `searchSpecies` → select → auto-fills scientific name, sunlight, watering notes. User can override.
|
|
- Care notes textareas: watering notes, fertilizing notes, general notes.
|
|
- Acquisition date picker.
|
|
- Image uploader: drag-and-drop or click, previews, max 10. First uploaded image auto-set as primary.
|
|
- `containerId` query-param pre-fill (from task 71 "Add plant to container" button).
|
|
|
|
**`species-search.tsx`** (client component)
|
|
|
|
- Debounced combobox calling a `/api/garden/species-search` route handler. Fires `onSelect(suggestion)` on pick.
|
|
|
|
### Pages (`src/app/garden/`)
|
|
|
|
- `plants/new/page.tsx` — `<PlantForm />` in create mode.
|
|
- `plants/[id]/page.tsx` — `<PlantDetail />` with edit/delete controls.
|
|
- `plants/[id]/edit/page.tsx` — `<PlantForm />` in edit mode.
|
|
|
|
### Activity log
|
|
|
|
`renderActivity` for `garden.plant`:
|
|
|
|
- `create` → `Added plant "${name}"`
|
|
- `update` → `Updated "${name}"`
|
|
- `delete` → `Removed plant "${name}"`
|
|
|
|
### Search
|
|
|
|
Register `search` on the `garden.plant` entity using `ilike` on `name || ' ' || coalesce(scientific_name, '')`.
|
|
|
|
### Share links
|
|
|
|
Plants are shareable (read-only). The shared view shows name, scientific name, primary image, gallery, health status, and care notes. Add `loadPlantForShare(id)` in `src/modules/garden/server/share-queries.ts`.
|
|
|
|
## Out of scope
|
|
|
|
- Care log UI and schedules (task 73).
|
|
- Dashboard widgets (task 75).
|
|
|
|
## Acceptance criteria
|
|
|
|
- [ ] Create / edit / delete plants works end-to-end.
|
|
- [ ] Species search returns Perenual results and auto-fills form fields on selection.
|
|
- [ ] App works correctly via manual entry when `PERENUAL_API_KEY` is absent.
|
|
- [ ] Up to 10 images upload; primary image can be set and changed; individual images can be removed.
|
|
- [ ] Plant list on `/garden` groups by container with an "Unassigned" bucket.
|
|
- [ ] `garden.plant` entity is registered with search and share support.
|
|
- [ ] Activity log entries appear for create / update / delete.
|
|
- [ ] Plant share link renders the read-only view correctly.
|