From 5cac8f326722b2da2bcb03d426aa114022f0394a Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sun, 11 Jan 2026 23:11:10 +0000 Subject: [PATCH] Add color-coded backgrounds to DecisionCard Per dashboard.md spec requirements: - RED background and text for REST decisions - YELLOW background and text for GENTLE/LIGHT/REDUCED decisions - GREEN background and text for TRAIN decisions Added 8 new tests for color-coded backgrounds (19 total). Updated IMPLEMENTATION_PLAN.md to mark spec gap as complete. Co-Authored-By: Claude Opus 4.5 --- IMPLEMENTATION_PLAN.md | 16 +-- .../dashboard/decision-card.test.tsx | 116 +++++++++++++++++- src/components/dashboard/decision-card.tsx | 32 ++++- 3 files changed, 148 insertions(+), 16 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 77af88b..a275e70 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -4,7 +4,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta ## Current State Summary -### Overall Status: 841 tests passing across 44 test files +### Overall Status: 849 tests passing across 44 test files ### Library Implementation | File | Status | Gap Analysis | @@ -113,7 +113,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/app/settings/page.test.tsx` | **EXISTS** - 34 tests (form rendering, validation, submission, accessibility, logout functionality) | | `src/app/api/auth/logout/route.test.ts` | **EXISTS** - 5 tests (cookie clearing, success response, error handling) | | `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) | -| `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 11 tests (rendering, status icons, styling) | +| `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 19 tests (rendering, status icons, styling, color-coded backgrounds) | | `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) | | `src/components/dashboard/nutrition-panel.test.tsx` | **EXISTS** - 12 tests (seeds, carbs, keto guidance) | | `src/components/dashboard/override-toggles.test.tsx` | **EXISTS** - 18 tests (toggle states, callbacks, styling) | @@ -702,18 +702,18 @@ Testing, error handling, and refinements. ### P3.11: Missing Component Tests ✅ COMPLETE - [x] Add unit tests for untested components - **Components Tested (5 total):** - - `src/components/dashboard/decision-card.tsx` - 11 tests for rendering decision status, icon, reason, styling + - `src/components/dashboard/decision-card.tsx` - 19 tests for rendering decision status, icon, reason, styling, color-coded backgrounds - `src/components/dashboard/data-panel.tsx` - 18 tests for biometrics display (BB, HRV, intensity), null handling, styling - `src/components/dashboard/nutrition-panel.tsx` - 12 tests for seeds, carbs, keto guidance display - `src/components/dashboard/override-toggles.tsx` - 18 tests for toggle states, callbacks, styling - `src/components/calendar/day-cell.tsx` - 23 tests for phase coloring, today highlighting, click handling - **Test Files Created:** - - `src/components/dashboard/decision-card.test.tsx` - 11 tests + - `src/components/dashboard/decision-card.test.tsx` - 19 tests - `src/components/dashboard/data-panel.test.tsx` - 18 tests - `src/components/dashboard/nutrition-panel.test.tsx` - 12 tests - `src/components/dashboard/override-toggles.test.tsx` - 18 tests - `src/components/calendar/day-cell.test.tsx` - 23 tests -- **Total Tests Added:** 82 tests across 5 files +- **Total Tests Added:** 90 tests across 5 files - **Why:** Component isolation ensures UI correctness and prevents regressions --- @@ -902,7 +902,7 @@ P4.* UX Polish ────────> After core functionality complete - [x] **metrics.ts** - Complete with 18 tests (metrics collection, counters, gauges, histograms, Prometheus format) (P2.16) ### Components -- [x] **DecisionCard** - Displays decision status, icon, and reason +- [x] **DecisionCard** - Displays decision status, icon, reason with color-coded backgrounds (RED/YELLOW/GREEN per status) - [x] **DataPanel** - Shows body battery, HRV, intensity data - [x] **NutritionPanel** - Shows seeds, carbs, keto guidance - [x] **OverrideToggles** - Toggle buttons for flare/stress/sleep/pms @@ -952,7 +952,7 @@ P4.* UX Polish ────────> After core functionality complete - [x] **P3.7: Error Handling Improvements** - Replaced console.error with structured pino logger across API routes, added key event logging (Period logged, Override toggled, Decision calculated, Auth failure), 3 new tests in auth-middleware.test.ts - [x] **P3.8: Loading States** - Complete with skeleton components (DecisionCardSkeleton, DataPanelSkeleton, NutritionPanelSkeleton, MiniCalendarSkeleton, OverrideTogglesSkeleton, CycleInfoSkeleton, DashboardSkeleton), 29 tests in skeletons.test.tsx; loading.tsx files for all routes (dashboard, calendar, history, plan, settings); shimmer animations matching spec requirements - [x] **P3.9: Token Expiration Warnings** - Complete with 10 new tests in email.test.ts, 10 new tests in garmin-sync/route.test.ts; sends warnings at 14 and 7 days before expiry -- [x] **P3.11: Missing Component Tests** - Complete with 82 tests across 5 component test files (DecisionCard: 11, DataPanel: 18, NutritionPanel: 12, OverrideToggles: 18, DayCell: 23) +- [x] **P3.11: Missing Component Tests** - Complete with 90 tests across 5 component test files (DecisionCard: 19, DataPanel: 18, NutritionPanel: 12, OverrideToggles: 18, DayCell: 23) ### P4: UX Polish and Accessibility - [x] **P4.1: Dashboard Onboarding Banners** - Complete with OnboardingBanner component (16 tests), dashboard integration (5 new tests) @@ -977,7 +977,7 @@ Analysis of all specs vs implementation revealed these gaps: | Garmin sync structured logging | observability.md | **COMPLETE** | Added sync start/complete/failure logging | | Email sent/failed logging | observability.md | **COMPLETE** | Email events now logged (info for success, error for failure) with structured data (userId, emailType, success) | | Period history UI | cycle-tracking.md | **PENDING** | UI for viewing/editing past periods | -| Dashboard color-coded backgrounds | dashboard.md | **PENDING** | Phase-based background colors | +| Dashboard color-coded backgrounds | dashboard.md | **COMPLETE** | DecisionCard shows RED/YELLOW/GREEN backgrounds per status (8 new tests) | | Toast notifications | dashboard.md | **PENDING** | Success/error toasts for user actions | | CI pipeline | testing.md | **PENDING** | GitHub Actions for test/lint/build | diff --git a/src/components/dashboard/decision-card.test.tsx b/src/components/dashboard/decision-card.test.tsx index b32a45e..4f287f6 100644 --- a/src/components/dashboard/decision-card.test.tsx +++ b/src/components/dashboard/decision-card.test.tsx @@ -122,6 +122,112 @@ describe("DecisionCard", () => { }); }); + describe("color-coded backgrounds", () => { + it("renders REST with red background", () => { + const decision: Decision = { + status: "REST", + reason: "Recovery day", + icon: "🛌", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("bg-red-100", "border-red-300"); + }); + + it("renders GENTLE with yellow background", () => { + const decision: Decision = { + status: "GENTLE", + reason: "Light movement", + icon: "🧘", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("bg-yellow-100", "border-yellow-300"); + }); + + it("renders LIGHT with yellow background", () => { + const decision: Decision = { + status: "LIGHT", + reason: "Moderate intensity", + icon: "🚶", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("bg-yellow-100", "border-yellow-300"); + }); + + it("renders REDUCED with yellow background", () => { + const decision: Decision = { + status: "REDUCED", + reason: "Lower intensity", + icon: "⬇️", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("bg-yellow-100", "border-yellow-300"); + }); + + it("renders TRAIN with green background", () => { + const decision: Decision = { + status: "TRAIN", + reason: "Full intensity approved", + icon: "🏃", + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass("bg-green-100", "border-green-300"); + }); + + it("renders reason with appropriate text color for REST", () => { + const decision: Decision = { + status: "REST", + reason: "Rest message", + icon: "🛌", + }; + + render(); + + const reason = screen.getByText("Rest message"); + expect(reason).toHaveClass("text-red-700"); + }); + + it("renders reason with appropriate text color for GENTLE", () => { + const decision: Decision = { + status: "GENTLE", + reason: "Gentle message", + icon: "🧘", + }; + + render(); + + const reason = screen.getByText("Gentle message"); + expect(reason).toHaveClass("text-yellow-700"); + }); + + it("renders reason with appropriate text color for TRAIN", () => { + const decision: Decision = { + status: "TRAIN", + reason: "Train message", + icon: "🏃", + }; + + render(); + + const reason = screen.getByText("Train message"); + expect(reason).toHaveClass("text-green-700"); + }); + }); + describe("styling", () => { it("renders within a bordered container", () => { const decision: Decision = { @@ -133,7 +239,7 @@ describe("DecisionCard", () => { const { container } = render(); const card = container.firstChild as HTMLElement; - expect(card).toHaveClass("rounded-lg", "border", "p-6"); + expect(card).toHaveClass("rounded-lg", "p-6"); }); it("renders status as bold heading", () => { @@ -150,17 +256,17 @@ describe("DecisionCard", () => { expect(heading).toHaveClass("font-bold"); }); - it("renders reason with muted color", () => { + it("renders reason with status-specific color", () => { const decision: Decision = { status: "REST", - reason: "Muted reason text", + reason: "Reason with status color", icon: "🛌", }; render(); - const reason = screen.getByText("Muted reason text"); - expect(reason).toHaveClass("text-gray-600"); + const reason = screen.getByText("Reason with status color"); + expect(reason).toHaveClass("text-red-700"); }); }); }); diff --git a/src/components/dashboard/decision-card.tsx b/src/components/dashboard/decision-card.tsx index b54e68f..5e34512 100644 --- a/src/components/dashboard/decision-card.tsx +++ b/src/components/dashboard/decision-card.tsx @@ -1,17 +1,43 @@ // ABOUTME: Dashboard card displaying today's training decision. -// ABOUTME: Shows decision status, icon, and reason prominently. +// ABOUTME: Shows decision status, icon, and reason with color-coded backgrounds. import type { Decision } from "@/types"; interface DecisionCardProps { decision: Decision; } +function getStatusColors(status: Decision["status"]): { + background: string; + text: string; +} { + switch (status) { + case "REST": + return { background: "bg-red-100 border-red-300", text: "text-red-700" }; + case "GENTLE": + case "LIGHT": + case "REDUCED": + return { + background: "bg-yellow-100 border-yellow-300", + text: "text-yellow-700", + }; + case "TRAIN": + return { + background: "bg-green-100 border-green-300", + text: "text-green-700", + }; + default: + return { background: "border", text: "text-gray-600" }; + } +} + export function DecisionCard({ decision }: DecisionCardProps) { + const colors = getStatusColors(decision.status); + return ( -
+
{decision.icon}

{decision.status}

-

{decision.reason}

+

{decision.reason}

); }