import { useEffect, useRef, useState } from "react"; import { api } from "../api/client"; import type { Garden as GardenData, GardenActivity } from "../api/types"; import { ru } from "../content/ru"; import { useDialog } from "../dialog/DialogContext"; import { useSession } from "../session/SessionContext"; import { Icon, type IconName } from "./Icon"; import { useToast } from "./Toast"; /* * «Ваш сад» (#119, slide 13): every useful activity grows the garden. Without pressure — it never wilts, does not * depend on answers, nobody competes; there are no streaks, no «missed» marks and no red. The numbers come from the * server (#118); this only draws them. */ const FLOWERS = ["#d9604c", "#e9a23b", "#c96f9a", "#e07a3f", "#b9577a", "#d4a02a"]; const SLOTS = [44, 92, 140, 188, 236, 284]; const GROUND = 128; const ICONS: Record = { meds: "pill", diary: "book", questionnaire: "clipboard" }; const seenKey = (token: string) => `ptsr.garden.steps.${token}`; function Plant({ x, stage, color, fresh }: { x: number; stage: number; color: string; fresh: boolean }) { if (stage <= 0) return ; const h = [0, 18, 38, 54, 62][Math.min(stage, 4)]; const top = GROUND - h; return ( {stage >= 2 && } {stage === 1 && } {stage === 3 && } {stage >= 4 && ( {[0, 72, 144, 216, 288].map((a) => ( ))} )} ); } export function Garden({ refreshKey }: { refreshKey?: unknown }) { const { session } = useSession(); const dialog = useDialog(); const toast = useToast(); const [garden, setGarden] = useState(null); const [freshFrom, setFreshFrom] = useState(null); const announced = useRef(false); useEffect(() => { if (!session) return; let alive = true; api .garden(session.token) .then((g) => { if (!alive) return; setGarden(g); const raw = window.localStorage.getItem(seenKey(session.token)); const seen = raw === null ? null : Number(raw); // No celebration right after a crisis: growth stays, the toast and the animation do not. The server knows // about alerts from scales and questionnaires too (`quiet`); the chat check covers a crisis reply just shown. const recentCrisis = dialog.entries.slice(-12).some((e) => e.from === "assistant" && e.reply.kind === "crisis"); if (seen !== null && g.steps > seen && !g.quiet && !recentCrisis) { setFreshFrom(seen); if (!announced.current) toast(ru.garden.grew); announced.current = true; } window.localStorage.setItem(seenKey(session.token), String(g.steps)); }) .catch(() => undefined); return () => { alive = false; }; }, [session, refreshKey]); if (!garden) return null; const t = ru.garden; const perPlant = 4; const bloom = garden.plants.filter((p) => p.stage >= perPlant).length; const growing = garden.plants.filter((p) => p.stage > 0 && p.stage < perPlant).length; const share = garden.points_in_step / garden.step_points; return (

{t.eyebrow}

{t.lead}

{garden.plants.map((p, i) => { // The plant that just grew (its stage covers steps after the last seen count) gets a gentle grow-in. const fresh = freshFrom !== null && garden.steps > freshFrom && Math.floor((garden.steps - 1) / perPlant) === i; return ( ); })}

{garden.title}

{t.next} {t.of(garden.points_in_step, garden.step_points)}
{garden.today.length > 0 && (
    {garden.today.map((item) => (
  • {item.title} {item.detail && · {item.detail}} {item.done && }
  • ))}
)}

{t.noPressure}

); }