v0.11.0 — Plant types: the reusable "kind" record

A rich-but-all-optional reference record per kind of plant (care, sun, water,
bloom & harvest windows, toxic-to-pets, edible parts, spacing), distinct from a
specific planting or a group.

- PlantType model + optional Plant.plantTypeId; migration + idempotent backfill
  that seeds 247 kinds from the built-in plant library and links plantings by
  species/common name (wired into the deploy entrypoint)
- /api/plant-types CRUD; new "Plant types" section (browse by category, edit a
  kind, see every spot you've planted it)
- New plants auto-link to their type on create; plant detail shows "More about…"
- PlantType added to backup/restore; bump APP_VERSION 0.11.0 + changelog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:46:07 -05:00
parent 9c0312676a
commit cdb21ed58a
20 changed files with 984 additions and 8 deletions

View File

@@ -8,6 +8,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.11.0",
date: "2026-06-15",
changes: [
"New \"Plant types\" library — a place to keep general info about each kind of plant (lily of the valley, Honeycrisp apple…): care notes, sun and water needs, bloom and harvest months, whether it's toxic to pets, and more. Every field is optional. Your plants link to their type automatically, so opening a plant now shows \"More about…\" and a type page lists every spot you've planted it.",
],
},
{
version: "0.10.0",
date: "2026-06-15",

View File

@@ -0,0 +1,63 @@
import type { PrismaClient } from "@prisma/client";
import { PLANT_DB } from "./plant-db";
// Stable, deterministic id for a library-seeded PlantType so upserts are
// idempotent across seed / deploy-backfill / re-runs.
function slug(s: string): string {
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
export function libraryTypeId(commonName: string, species?: string | null): string {
return `pt-${slug(commonName)}${species ? `-${slug(species)}` : ""}`;
}
export type PlantTypeSyncResult = { types: number; linked: number };
/**
* Idempotently upsert a PlantType for every entry in the built-in plant library
* (src/lib/garden/plant-db.ts), then link any Plant lacking a plantTypeId to its
* kind — by species first, else common name. `update: {}` so re-runs never
* clobber a user's edits to a type. Safe to run repeatedly.
*/
export async function syncPlantTypesFromLibrary(db: PrismaClient): Promise<PlantTypeSyncResult> {
for (const entry of PLANT_DB) {
const id = libraryTypeId(entry.commonName, entry.species);
await db.plantType.upsert({
where: { id },
update: {},
create: {
id,
commonName: entry.commonName,
species: entry.species ?? null,
category: entry.category,
careNotes: entry.notes ?? null,
},
});
}
const types = await db.plantType.findMany({
select: { id: true, commonName: true, species: true },
});
const bySpecies = new Map<string, string>();
const byCommon = new Map<string, string>();
for (const t of types) {
if (t.species) bySpecies.set(t.species.toLowerCase().trim(), t.id);
byCommon.set(t.commonName.toLowerCase().trim(), t.id);
}
const unlinked = await db.plant.findMany({
where: { plantTypeId: null },
select: { id: true, commonName: true, species: true },
});
let linked = 0;
for (const p of unlinked) {
const typeId =
(p.species && bySpecies.get(p.species.toLowerCase().trim())) ||
byCommon.get(p.commonName.toLowerCase().trim());
if (typeId) {
await db.plant.update({ where: { id: p.id }, data: { plantTypeId: typeId } });
linked++;
}
}
return { types: PLANT_DB.length, linked };
}

View File

@@ -1,2 +1,2 @@
// Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`.
export const APP_VERSION = "0.10.0";
export const APP_VERSION = "0.11.0";