diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md
index 2445177..6ba9483 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: 950 unit tests passing across 49 test files + 64 E2E tests across 6 files
+### Overall Status: 977 unit tests passing across 50 test files + 64 E2E tests across 6 files
### Library Implementation
| File | Status | Gap Analysis |
@@ -130,7 +130,8 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
| `src/components/dashboard/onboarding-banner.test.tsx` | **EXISTS** - 16 tests (setup prompts, icons, action buttons, interactions, dismissal) |
| `src/components/calendar/day-cell.test.tsx` | **EXISTS** - 27 tests (phase coloring, today highlighting, click handling, accessibility) |
| `src/app/plan/page.test.tsx` | **EXISTS** - 16 tests (loading states, error handling, phase display, exercise reference, rebounding techniques) |
-| `src/app/layout.test.tsx` | **EXISTS** - 3 tests (skip navigation link rendering, accessibility) |
+| `src/app/layout.test.tsx` | **EXISTS** - 4 tests (skip navigation link rendering, accessibility, Toaster rendering) |
+| `src/components/ui/toaster.test.tsx` | **EXISTS** - 23 tests (toast rendering, types, auto-dismiss, error persistence, accessibility) |
### Critical Business Rules (from Spec)
1. **Override Priority:** flare > stress > sleep > pms (must be enforced in order)
@@ -878,9 +879,9 @@ P4.* UX Polish ────────> After core functionality complete
| Done | P5.1 Period History UI | Complete | Page + 3 API routes with 61 tests |
| Done | P5.3 CI Pipeline | Complete | Lint, typecheck, tests in Gitea Actions |
| Done | P5.4 E2E Tests | Complete | 64 tests across 6 files |
-| **Low** | P5.2 Toast Notifications | Low | Install library + integrate |
+| Done | P5.2 Toast Notifications | Complete | sonner library + 23 tests |
-**All P0-P4 items are complete. P5.1, P5.3, and P5.4 complete. Only remaining P5 item: Toast Notifications.**
+**All P0-P5 items are complete. The project is feature complete.**
@@ -991,16 +992,16 @@ Analysis of all specs vs implementation revealed these gaps:
| Logout functionality | authentication.md | **COMPLETE** | Added POST /api/auth/logout + settings button |
| 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** | See P5.1 below |
+| Period history UI | cycle-tracking.md | **COMPLETE** | See P5.1 below |
| Dashboard color-coded backgrounds | dashboard.md | **COMPLETE** | DecisionCard shows RED/YELLOW/GREEN backgrounds per status (8 new tests) |
-| Toast notifications | dashboard.md | **PENDING** | See P5.2 below |
-| CI pipeline | testing.md | **PARTIALLY COMPLETE** | See P5.3 below |
+| Toast notifications | dashboard.md | **COMPLETE** | sonner library + Toaster component + showToast utility (23 tests) |
+| CI pipeline | testing.md | **COMPLETE** | See P5.3 below |
---
-## P5: Remaining Gaps
+## P5: Final Items ✅ ALL COMPLETE
-These items were identified during gap analysis and remain pending.
+These items were identified during gap analysis and have been completed.
### P5.1: Period History UI ✅ COMPLETE
- [x] Create period history viewing and editing UI
@@ -1037,36 +1038,34 @@ These items were identified during gap analysis and remain pending.
- **Total Tests Added:** 61 tests (18 + 16 + 27)
- **Why:** Users need to view and correct their period log history per spec requirement
-### P5.2: Toast Notifications (PENDING)
-- [ ] Add toast notification system for user feedback
+### P5.2: Toast Notifications ✅ COMPLETE
+- [x] Add toast notification system for user feedback
- **Spec Reference:** specs/dashboard.md lines 88-96
-- **Required Features:**
+- **Features Implemented:**
- Toast position: Bottom-right
- - Auto-dismiss after 5 seconds
- - Errors persist until dismissed
+ - Auto-dismiss after 5 seconds for success/info toasts
+ - Error toasts persist until dismissed (per spec: "Errors persist until dismissed")
- Toast types: success, error, info
-- **Example Messages (from spec):**
- - Network errors: "Unable to fetch data. Retry?"
- - Garmin sync failed: "Garmin data unavailable. Using last known values."
- - Save errors: "Failed to save. Try again."
-- **Current State:**
- - Only inline error messages exist
- - No toast library installed
- - No toast component
-- **Implementation Tasks:**
- 1. Install toast library (react-hot-toast or sonner recommended)
- 2. Create Toast provider in layout.tsx
- 3. Create useToast hook for consistent API
- 4. Replace inline errors with toast notifications across:
- - Dashboard (override toggle errors)
- - Settings (save success/error)
- - Garmin settings (connection success/error)
- - Calendar (token regeneration success/error)
- - Period logging (confirmation/error)
-- **Files to Create/Modify:**
- - `src/components/ui/toaster.tsx` + tests
- - `src/app/layout.tsx` (add provider)
- - Various page components (replace inline errors)
+ - Dark mode support with proper color theming
+ - Close button on all toasts
+- **Library Used:** sonner (v2.0.7)
+- **Files Created/Modified:**
+ - `src/components/ui/toaster.tsx` - Toaster component wrapping sonner with showToast utility (23 tests)
+ - `src/app/layout.tsx` - Added Toaster provider
+ - `src/app/page.tsx` - Dashboard override toggle errors now use toast
+ - `src/app/settings/page.tsx` - Settings save/load errors now use toast
+ - `src/app/settings/garmin/page.tsx` - Garmin connection success/error now use toast
+ - `src/app/calendar/page.tsx` - Calendar load errors now use toast
+ - `src/app/period-history/page.tsx` - Period history load/delete errors now use toast
+- **Test Files Updated:**
+ - `src/components/ui/toaster.test.tsx` - 23 tests for toast component
+ - `src/app/layout.test.tsx` - Added Toaster mock
+ - `src/app/page.test.tsx` - Added showToast mock and test
+ - `src/app/settings/page.test.tsx` - Added showToast mock
+ - `src/app/settings/garmin/page.test.tsx` - Added showToast mock
+ - `src/app/calendar/page.test.tsx` - Added showToast mock and test
+ - `src/app/period-history/page.test.tsx` - Added showToast mock and tests
+- **Total Tests Added:** 27 new tests (23 toaster + 4 integration tests across pages)
- **Why:** Better UX for transient feedback per spec requirements
### P5.3: CI Pipeline ✅ COMPLETE
diff --git a/package.json b/package.json
index 65b06e7..5d586b5 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"resend": "^6.7.0",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.5"
},
@@ -39,6 +40,7 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
"@types/node-cron": "^3.0.11",
"@types/react": "^19",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c0fa705..efcdfe7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,6 +47,9 @@ importers:
resend:
specifier: ^6.7.0
version: 6.7.0
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
@@ -72,6 +75,9 @@ importers:
'@testing-library/react':
specifier: ^16.3.1
version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/node':
specifier: ^20
version: 20.19.27
@@ -1230,6 +1236,12 @@ packages:
'@types/react-dom':
optional: true
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -1911,6 +1923,12 @@ packages:
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2934,6 +2952,10 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@@ -3608,6 +3630,11 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
+ sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
source-map-js@1.2.1: {}
source-map-support@0.5.21:
diff --git a/src/app/calendar/page.test.tsx b/src/app/calendar/page.test.tsx
index aa36da5..52a2be5 100644
--- a/src/app/calendar/page.test.tsx
+++ b/src/app/calendar/page.test.tsx
@@ -11,6 +11,16 @@ vi.mock("next/navigation", () => ({
}),
}));
+// Mock showToast utility with vi.hoisted to avoid hoisting issues
+const mockShowToast = vi.hoisted(() => ({
+ success: vi.fn(),
+ error: vi.fn(),
+ info: vi.fn(),
+}));
+vi.mock("@/components/ui/toaster", () => ({
+ showToast: mockShowToast,
+}));
+
// Mock fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
@@ -30,6 +40,9 @@ describe("CalendarPage", () => {
beforeEach(() => {
vi.clearAllMocks();
+ mockShowToast.success.mockClear();
+ mockShowToast.error.mockClear();
+ mockShowToast.info.mockClear();
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUser),
@@ -134,6 +147,21 @@ describe("CalendarPage", () => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
});
+
+ it("shows error toast when fetching fails", async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ json: () => Promise.resolve({ error: "Network error" }),
+ });
+
+ render(