t.title)}
+ />
+
{selected && (
diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts
index 6be537e..04e68b6 100644
--- a/src/lib/changelog.ts
+++ b/src/lib/changelog.ts
@@ -8,6 +8,15 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.27.0",
+ date: "2026-07-06",
+ changes: [
+ "The garden got a brain! The Calendar now shows \"Good to do this month\" — timely advice matched to what you actually grow, tuned for our zone (4b, ~May 15 last frost): when to prune the raspberries, start tomato seeds, plant garlic, divide the irises, and more. Each tip has a \"Remind me\" button that turns it into a yearly reminder, and a link to read more at UW–Madison Extension.",
+ "Plant type pages now show a \"Growing in zone 4b\" card: the care timeline for that plant, what it grows well next to, what to keep it away from, and its crop-rotation family (so you know not to plant tomatoes where the potatoes just were).",
+ "This starts with 28 common crops, fruits, and flowers — it'll grow over time.",
+ ],
+ },
{
version: "0.26.0",
date: "2026-07-06",
diff --git a/src/lib/garden/knowledge.test.ts b/src/lib/garden/knowledge.test.ts
new file mode 100644
index 0000000..7a145a8
--- /dev/null
+++ b/src/lib/garden/knowledge.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from "vitest";
+import { knowledgeFor, tipsForMonth, uwSearchUrl, KNOWLEDGE } from "./knowledge";
+
+describe("knowledgeFor", () => {
+ it("matches exact and case-insensitive names", () => {
+ expect(knowledgeFor("Tomato")?.name).toBe("Tomato");
+ expect(knowledgeFor("raspberry")?.name).toBe("Raspberry");
+ });
+
+ it("matches plurals and richer plant names", () => {
+ expect(knowledgeFor("Raspberries")?.name).toBe("Raspberry");
+ expect(knowledgeFor("Heritage Raspberry")?.name).toBe("Raspberry");
+ expect(knowledgeFor("Bearded Iris")?.name).toBe("Iris");
+ });
+
+ it("matches via aliases", () => {
+ expect(knowledgeFor("Zucchini")?.name).toBe("Squash");
+ expect(knowledgeFor("Kale")?.name).toBe("Broccoli");
+ expect(knowledgeFor("Pear")?.name).toBe("Apple");
+ });
+
+ it("returns null for unknown plants", () => {
+ expect(knowledgeFor("Dusty Miller")).toBeNull();
+ });
+});
+
+describe("tipsForMonth", () => {
+ it("only suggests for plants actually grown", () => {
+ const march = tipsForMonth(3, ["Raspberry", "Bearded Iris"]);
+ const actions = march.map((t) => t.action);
+ expect(actions).toContain("Prune raspberries");
+ expect(actions).not.toContain("Prune apple and pear trees");
+ });
+
+ it("includes general homestead tips regardless of plants", () => {
+ const nov = tipsForMonth(11, []);
+ expect(nov.some((t) => t.crop === null && t.action.includes("Winter-mulch"))).toBe(true);
+ });
+
+ it("dedupes when several plants match the same crop", () => {
+ const july = tipsForMonth(7, ["Bearded Iris", "Iris sibirica", "Yellow Flag Iris"]);
+ expect(july.filter((t) => t.crop === "Iris")).toHaveLength(1);
+ });
+
+ it("every suggestion carries a UW link", () => {
+ for (const t of tipsForMonth(6, ["Tomato", "Garlic", "Lilac"])) {
+ expect(t.uwUrl).toMatch(/^https:\/\/hort\.extension\.wisc\.edu\/\?s=/);
+ }
+ });
+});
+
+describe("knowledge data sanity", () => {
+ it("all months are 1–12 and every crop has tips + a query", () => {
+ for (const crop of KNOWLEDGE) {
+ expect(crop.tips.length).toBeGreaterThan(0);
+ expect(crop.uwQuery.length).toBeGreaterThan(0);
+ for (const tip of crop.tips) {
+ for (const m of tip.months) {
+ expect(m).toBeGreaterThanOrEqual(1);
+ expect(m).toBeLessThanOrEqual(12);
+ }
+ }
+ }
+ });
+
+ it("uwSearchUrl encodes queries", () => {
+ expect(uwSearchUrl("growing tomatoes")).toBe("https://hort.extension.wisc.edu/?s=growing%20tomatoes");
+ });
+});
diff --git a/src/lib/garden/knowledge.ts b/src/lib/garden/knowledge.ts
new file mode 100644
index 0000000..3e6f70e
--- /dev/null
+++ b/src/lib/garden/knowledge.ts
@@ -0,0 +1,435 @@
+// The garden brain — curated zone-4b (Eau Claire, WI) growing knowledge:
+// month-by-month care timing, companion friends/foes, and crop-rotation
+// families. Timing assumes ~May 15 last frost and ~Oct 1 first frost.
+//
+// This is starter reference data distilled from standard extension guidance;
+// every entry links to a UW–Madison Extension site search (uwUrl) so the full
+// article is one click away. Curated by hand — extend freely, keep it honest.
+
+export type RotationFamily =
+ | "nightshade" | "legume" | "brassica" | "allium" | "cucurbit"
+ | "root" | "leafy" | "corn" | "perennial";
+
+export type CareTip = {
+ months: number[]; // 1–12, zone 4b
+ action: string; // short imperative — becomes a suggested reminder title
+ detail?: string;
+};
+
+export type CropKnowledge = {
+ name: string;
+ aliases?: string[];
+ family?: RotationFamily;
+ friends?: string[];
+ foes?: string[];
+ rotationNote?: string;
+ tips: CareTip[];
+ uwQuery: string;
+};
+
+export function uwSearchUrl(query: string): string {
+ return `https://hort.extension.wisc.edu/?s=${encodeURIComponent(query)}`;
+}
+
+export const ROTATION_LABELS: Record = {
+ nightshade: "Nightshades (tomato, pepper, potato, eggplant)",
+ legume: "Legumes (beans, peas)",
+ brassica: "Brassicas (cabbage, broccoli, kale…)",
+ allium: "Alliums (onion, garlic, leek)",
+ cucurbit: "Cucurbits (squash, cucumber, melon)",
+ root: "Roots (carrot, beet, radish)",
+ leafy: "Leafy greens (lettuce, spinach, chard)",
+ corn: "Corn",
+ perennial: "Perennial (stays put — no rotation)",
+};
+
+export const KNOWLEDGE: CropKnowledge[] = [
+ {
+ name: "Tomato",
+ family: "nightshade",
+ friends: ["Basil", "Carrot", "Onion", "Marigold"],
+ foes: ["Cabbage family", "Fennel", "Potato", "Corn"],
+ rotationNote: "Don't replant where tomatoes, peppers, or potatoes grew in the last 3 years — shared soil diseases build up.",
+ uwQuery: "growing tomatoes",
+ tips: [
+ { months: [4], action: "Start tomato seeds indoors", detail: "6 weeks before the ~May 15 frost date." },
+ { months: [5, 6], action: "Harden off and transplant tomatoes", detail: "Out after frost danger — late May into June." },
+ { months: [6, 7, 8], action: "Water tomatoes deeply and evenly", detail: "Uneven watering invites blossom-end rot; mulch helps." },
+ { months: [9], action: "Pick remaining green tomatoes before frost", detail: "They ripen indoors on the counter." },
+ ],
+ },
+ {
+ name: "Pepper",
+ family: "nightshade",
+ friends: ["Basil", "Onion"],
+ foes: ["Fennel"],
+ rotationNote: "Rotate with the other nightshades — 3 years out of the same bed.",
+ uwQuery: "growing peppers",
+ tips: [
+ { months: [3, 4], action: "Start pepper seeds indoors", detail: "Peppers are slow — 8 weeks before frost." },
+ { months: [6], action: "Transplant peppers", detail: "They sulk in cold soil; wait for warm June nights." },
+ ],
+ },
+ {
+ name: "Potato",
+ family: "nightshade",
+ friends: ["Beans", "Corn", "Cabbage"],
+ foes: ["Tomato", "Squash", "Cucumber", "Sunflower"],
+ rotationNote: "Shares blight with tomatoes — keep them apart and rotate 3 years.",
+ uwQuery: "growing potatoes",
+ tips: [
+ { months: [5], action: "Plant seed potatoes", detail: "Mid-May, once soil works easily." },
+ { months: [6, 7], action: "Hill potatoes", detail: "Mound soil over the row so tubers never see sun." },
+ { months: [9], action: "Dig potatoes", detail: "After the vines die back; cure before storing." },
+ ],
+ },
+ {
+ name: "Bean",
+ aliases: ["Green bean", "Pole bean", "Bush bean"],
+ family: "legume",
+ friends: ["Corn", "Squash", "Carrot"],
+ foes: ["Onion", "Garlic", "Leek"],
+ rotationNote: "Legumes feed the soil — a great crop to grow before heavy feeders like brassicas or corn.",
+ uwQuery: "growing beans",
+ tips: [
+ { months: [5, 6], action: "Direct-sow beans", detail: "After frost; succession-sow every 2 weeks for a steady picking." },
+ ],
+ },
+ {
+ name: "Pea",
+ family: "legume",
+ friends: ["Carrot", "Radish"],
+ foes: ["Onion", "Garlic"],
+ uwQuery: "growing peas",
+ tips: [
+ { months: [4, 5], action: "Direct-sow peas", detail: "As early as the soil can be worked — peas love cold feet." },
+ ],
+ },
+ {
+ name: "Garlic",
+ family: "allium",
+ friends: ["Tomato", "Cabbage family"],
+ foes: ["Beans", "Peas"],
+ uwQuery: "growing garlic",
+ tips: [
+ { months: [10], action: "Plant garlic cloves", detail: "Mid-October, mulched well — it overwinters." },
+ { months: [6], action: "Snap off garlic scapes", detail: "Sends energy to the bulb; scapes are dinner." },
+ { months: [7], action: "Lift garlic", detail: "When the lower third of leaves have browned; cure in shade." },
+ ],
+ },
+ {
+ name: "Onion",
+ aliases: ["Shallot", "Leek"],
+ family: "allium",
+ friends: ["Carrot", "Beet", "Cabbage family"],
+ foes: ["Beans", "Peas"],
+ uwQuery: "growing onions",
+ tips: [
+ { months: [4, 5], action: "Set out onion plants or sets", detail: "Onions size up on day length — get them in early." },
+ { months: [8, 9], action: "Harvest onions", detail: "When tops flop over; cure until necks are papery." },
+ ],
+ },
+ {
+ name: "Carrot",
+ family: "root",
+ friends: ["Onion", "Tomato", "Pea"],
+ foes: ["Dill"],
+ uwQuery: "growing carrots",
+ tips: [
+ { months: [4, 5, 6], action: "Sow carrots", detail: "Keep the seedbed moist until they're up — it takes patience." },
+ { months: [10], action: "Finish digging carrots", detail: "A light frost sweetens them; dig before the ground locks up." },
+ ],
+ },
+ {
+ name: "Beet",
+ family: "root",
+ friends: ["Onion", "Cabbage family"],
+ foes: ["Pole beans"],
+ uwQuery: "growing beets",
+ tips: [
+ { months: [4, 5, 6, 7], action: "Sow beets", detail: "Succession-sow into midsummer for fall roots." },
+ ],
+ },
+ {
+ name: "Lettuce",
+ aliases: ["Salad greens"],
+ family: "leafy",
+ friends: ["Carrot", "Radish"],
+ uwQuery: "growing lettuce",
+ tips: [
+ { months: [4, 5], action: "Sow lettuce", detail: "Cool-season crop; shade cloth stretches it into summer." },
+ { months: [8], action: "Sow fall lettuce", detail: "Late-summer sowing beats the September chill." },
+ ],
+ },
+ {
+ name: "Spinach",
+ family: "leafy",
+ uwQuery: "growing spinach",
+ tips: [
+ { months: [4], action: "Sow spinach", detail: "Bolts in heat — spring and fall are its seasons here." },
+ { months: [8, 9], action: "Sow fall spinach", detail: "Overwinters under mulch for the earliest spring greens." },
+ ],
+ },
+ {
+ name: "Broccoli",
+ aliases: ["Cabbage", "Cauliflower", "Kale", "Brussels sprouts", "Kohlrabi"],
+ family: "brassica",
+ friends: ["Onion", "Dill", "Chamomile"],
+ foes: ["Tomato", "Strawberry"],
+ rotationNote: "Rotate brassicas on a 3-year cycle to dodge clubroot and cabbage maggots.",
+ uwQuery: "growing broccoli cabbage",
+ tips: [
+ { months: [4], action: "Start brassica seeds indoors", detail: "Cabbage, broccoli, kale — 4–5 weeks before transplanting." },
+ { months: [5], action: "Transplant brassicas", detail: "They shrug off light frost; row covers stop the cabbage moths." },
+ { months: [7], action: "Sow fall brassicas", detail: "A July sowing makes the best autumn broccoli and kale." },
+ ],
+ },
+ {
+ name: "Squash",
+ aliases: ["Zucchini", "Pumpkin", "Winter squash"],
+ family: "cucurbit",
+ friends: ["Corn", "Beans", "Nasturtium"],
+ foes: ["Potato"],
+ uwQuery: "growing squash",
+ tips: [
+ { months: [6], action: "Sow or transplant squash", detail: "Warm soil only — early June here." },
+ { months: [9, 10], action: "Harvest winter squash before hard frost", detail: "Leave an inch of stem; cure 2 weeks for storage." },
+ ],
+ },
+ {
+ name: "Cucumber",
+ family: "cucurbit",
+ friends: ["Beans", "Dill"],
+ foes: ["Potato"],
+ uwQuery: "growing cucumbers",
+ tips: [
+ { months: [6], action: "Sow cucumbers", detail: "Direct-sow in warm June soil; trellis for straight fruit." },
+ ],
+ },
+ {
+ name: "Corn",
+ aliases: ["Sweet corn"],
+ family: "corn",
+ friends: ["Beans", "Squash"],
+ foes: ["Tomato"],
+ uwQuery: "growing sweet corn",
+ tips: [
+ { months: [5, 6], action: "Sow sweet corn in blocks", detail: "Blocks, not rows — corn is wind-pollinated." },
+ ],
+ },
+ {
+ name: "Strawberry",
+ family: "perennial",
+ foes: ["Cabbage family"],
+ rotationNote: "Avoid beds that recently grew tomatoes/potatoes (verticillium lingers).",
+ uwQuery: "growing strawberries",
+ tips: [
+ { months: [5], action: "Plant strawberries", detail: "Pinch first-year blossoms on June-bearers — bigger crops after." },
+ { months: [7], action: "Renovate June-bearing strawberries", detail: "Right after harvest: mow, narrow the rows, fertilize." },
+ { months: [11], action: "Mulch strawberries for winter", detail: "Straw on after the ground starts to freeze." },
+ ],
+ },
+ {
+ name: "Raspberry",
+ family: "perennial",
+ foes: ["Potato"],
+ rotationNote: "Keep away from where nightshades grew — verticillium wilt.",
+ uwQuery: "growing raspberries",
+ tips: [
+ { months: [3, 4], action: "Prune raspberries", detail: "Remove last year's spent canes; thin the rest to the sturdiest." },
+ { months: [7, 8, 9], action: "Pick raspberries often", detail: "Every couple of days keeps the patch producing." },
+ ],
+ },
+ {
+ name: "Blueberry",
+ family: "perennial",
+ uwQuery: "growing blueberries",
+ tips: [
+ { months: [3], action: "Prune blueberries", detail: "While dormant; remove the oldest canes." },
+ { months: [5], action: "Check blueberry soil pH", detail: "They need acid soil (pH 4.5–5.5) — sulfur takes months to work." },
+ ],
+ },
+ {
+ name: "Currant",
+ aliases: ["Gooseberry"],
+ family: "perennial",
+ uwQuery: "growing currants gooseberries",
+ tips: [
+ { months: [3], action: "Prune currants and gooseberries", detail: "Keep a mix of 1–3 year old stems; remove older wood." },
+ { months: [7], action: "Pick currants", detail: "Whole strigs at once — easier to sort in the kitchen." },
+ ],
+ },
+ {
+ name: "Grape",
+ family: "perennial",
+ uwQuery: "pruning grapes",
+ tips: [
+ { months: [3], action: "Prune grape vines", detail: "Hard, while fully dormant — grapes fruit on new wood from year-old canes." },
+ ],
+ },
+ {
+ name: "Apple",
+ aliases: ["Pear"],
+ family: "perennial",
+ uwQuery: "apple tree care",
+ tips: [
+ { months: [3], action: "Prune apple and pear trees", detail: "Late winter, before buds break — open the center to light." },
+ { months: [6], action: "Thin apples", detail: "One fruit per cluster, a hand-width apart; bigger, healthier apples." },
+ { months: [11], action: "Guard young fruit trees for winter", detail: "Trunk guards against voles and rabbits; pull mulch back from bark." },
+ ],
+ },
+ {
+ name: "Cherry",
+ aliases: ["Plum", "Stone fruit"],
+ family: "perennial",
+ uwQuery: "growing cherries plums",
+ tips: [
+ { months: [3], action: "Prune stone fruit trees", detail: "Late dormant season, on a dry day." },
+ { months: [7], action: "Net cherries against birds", detail: "The birds are watching the same calendar you are." },
+ ],
+ },
+ {
+ name: "Rhubarb",
+ family: "perennial",
+ uwQuery: "growing rhubarb",
+ tips: [
+ { months: [5, 6], action: "Harvest rhubarb", detail: "Pull stalks, don't cut; stop by July so the crown recharges." },
+ { months: [9], action: "Divide crowded rhubarb", detail: "Every 5 years or so, when stalks get thin." },
+ ],
+ },
+ {
+ name: "Asparagus",
+ family: "perennial",
+ uwQuery: "growing asparagus",
+ tips: [
+ { months: [5, 6], action: "Harvest asparagus", detail: "Snap spears until mid-June, then let the ferns grow — they feed next year." },
+ { months: [6], action: "Fertilize asparagus after harvest", detail: "The ferns do the work for next spring." },
+ { months: [11], action: "Cut down asparagus ferns", detail: "After they brown; removes overwintering asparagus-beetle habitat." },
+ ],
+ },
+ {
+ name: "Basil",
+ family: "leafy",
+ friends: ["Tomato"],
+ uwQuery: "growing basil",
+ tips: [
+ { months: [4], action: "Start basil indoors", detail: "It hates cold — never rush it outside." },
+ { months: [6, 7, 8], action: "Pinch basil tops", detail: "Pinch before it flowers to keep leaves coming." },
+ ],
+ },
+ {
+ name: "Iris",
+ aliases: ["Bearded iris"],
+ family: "perennial",
+ uwQuery: "iris care dividing",
+ tips: [
+ { months: [7, 8], action: "Divide crowded irises", detail: "4–6 weeks after bloom; discard old center rhizomes (check for borers)." },
+ { months: [10], action: "Cut iris foliage back", detail: "Fall cleanup breaks the iris-borer cycle." },
+ ],
+ },
+ {
+ name: "Peony",
+ family: "perennial",
+ uwQuery: "peony care",
+ tips: [
+ { months: [9], action: "Plant or divide peonies", detail: "Eyes no deeper than 2 inches — planting deep is why peonies sulk." },
+ { months: [10], action: "Cut peonies to the ground", detail: "Fall removal keeps botrytis from overwintering." },
+ ],
+ },
+ {
+ name: "Lilac",
+ aliases: ["Forsythia", "Spring-flowering shrub"],
+ family: "perennial",
+ uwQuery: "pruning lilacs",
+ tips: [
+ { months: [6], action: "Prune lilacs right after bloom", detail: "They bloom on old wood — prune in fall/winter and you cut off next year's flowers." },
+ ],
+ },
+ {
+ name: "Bee balm",
+ aliases: ["Coneflower", "Black-eyed Susan", "Perennial flower"],
+ family: "perennial",
+ uwQuery: "dividing perennials",
+ tips: [
+ { months: [5], action: "Divide summer perennials", detail: "Spring division for the ones that bloom after midsummer." },
+ { months: [7, 8], action: "Deadhead perennial flowers", detail: "Keeps the show going and self-seeding polite." },
+ ],
+ },
+];
+
+// Homestead-wide reminders that apply regardless of what's planted.
+export const GENERAL_TIPS: (CareTip & { uwQuery: string })[] = [
+ { months: [3], action: "Set up seed-starting lights", detail: "The zone-4 season starts indoors in March.", uwQuery: "starting seeds indoors" },
+ { months: [5], action: "Watch for late frost", detail: "Eau Claire's average last frost is ~May 15; keep covers handy.", uwQuery: "frost protection" },
+ { months: [6], action: "Mulch the vegetable beds", detail: "After soil warms: holds water, blocks weeds.", uwQuery: "mulching garden" },
+ { months: [9], action: "Sow a cover crop in empty beds", detail: "Oats or rye protect and feed the soil over winter.", uwQuery: "cover crops garden" },
+ { months: [10], action: "Fall garden cleanup", detail: "Remove diseased plant debris (compost the healthy stuff).", uwQuery: "fall garden cleanup" },
+ { months: [11], action: "Winter-mulch perennials", detail: "After the ground freezes, not before — the goal is keeping it frozen.", uwQuery: "winter mulch perennials" },
+];
+
+// ---------------------------------------------------------------------------
+// Matching — connect knowledge entries to the plants Bonna actually grows.
+// ---------------------------------------------------------------------------
+
+function norm(s: string): string {
+ return s.toLowerCase().replace(/[^a-z ]/g, "").trim();
+}
+
+/**
+ * Whole-word, plural-tolerant match: "Raspberries" hits "Raspberry",
+ * "Bearded Iris" hits "Iris" — but "Pear" must never hit "Pea".
+ */
+function nameMatches(knowledgeName: string, plantName: string): boolean {
+ const stem = (w: string) => (w.endsWith("ies") ? w.slice(0, -3) + "y" : w.replace(/e?s$/, ""));
+ const kStems = norm(knowledgeName).split(" ").filter(Boolean).map(stem);
+ const pStems = norm(plantName).split(" ").filter(Boolean).map(stem);
+ if (kStems.length === 0 || pStems.length === 0) return false;
+ return kStems.every((w) => pStems.includes(w));
+}
+
+/** The knowledge entry for a plant/type name, if we have one. */
+export function knowledgeFor(plantName: string): CropKnowledge | null {
+ for (const crop of KNOWLEDGE) {
+ if (nameMatches(crop.name, plantName)) return crop;
+ if (crop.aliases?.some((a) => nameMatches(a, plantName))) return crop;
+ }
+ return null;
+}
+
+export type MonthlySuggestion = {
+ crop: string | null; // null = general homestead tip
+ action: string;
+ detail?: string;
+ uwUrl: string;
+};
+
+/**
+ * What's worth doing this month (1–12), given the plant names on hand.
+ * General tips always apply; crop tips only if a matching plant is grown.
+ */
+export function tipsForMonth(month: number, ownedNames: string[]): MonthlySuggestion[] {
+ const out: MonthlySuggestion[] = [];
+ const seen = new Set();
+
+ for (const crop of KNOWLEDGE) {
+ const grown = ownedNames.some(
+ (n) => nameMatches(crop.name, n) || crop.aliases?.some((a) => nameMatches(a, n)),
+ );
+ if (!grown) continue;
+ for (const tip of crop.tips) {
+ if (!tip.months.includes(month)) continue;
+ const key = `${crop.name}|${tip.action}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push({ crop: crop.name, action: tip.action, detail: tip.detail, uwUrl: uwSearchUrl(crop.uwQuery) });
+ }
+ }
+
+ for (const tip of GENERAL_TIPS) {
+ if (tip.months.includes(month)) {
+ out.push({ crop: null, action: tip.action, detail: tip.detail, uwUrl: uwSearchUrl(tip.uwQuery) });
+ }
+ }
+
+ return out;
+}
diff --git a/src/lib/version.ts b/src/lib/version.ts
index 2bdb10a..aa50d98 100644
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -1,2 +1,2 @@
// Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`.
-export const APP_VERSION = "0.26.0";
+export const APP_VERSION = "0.27.0";