Add toast notification system with sonner library

- Create Toaster component wrapping sonner at bottom-right position
- Add showToast utility with success/error/info methods
- Error toasts persist until dismissed, others auto-dismiss after 5s
- Migrate error handling to toasts across all pages:
  - Dashboard (override toggle errors)
  - Settings (save/load success/error)
  - Garmin settings (connection success/error)
  - Calendar (load errors)
  - Period History (load/delete errors)
- Add dark mode support for toast styling
- Add Toaster provider to root layout
- 27 new tests (23 toaster component + 4 integration)
- Total: 977 unit tests passing

P5.2 COMPLETE - All P0-P5 items now complete.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 23:04:27 +00:00
parent 38bea1ffd7
commit e971fe683f
17 changed files with 697 additions and 187 deletions

View File

@@ -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(<CalendarPage />);
await waitFor(() => {
expect(mockShowToast.error).toHaveBeenCalledWith(
"Unable to fetch data. Retry?",
);
});
});
});
describe("month navigation", () => {

View File

@@ -5,6 +5,7 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { MonthView } from "@/components/calendar/month-view";
import { showToast } from "@/components/ui/toaster";
interface User {
id: string;
@@ -30,12 +31,15 @@ export default function CalendarPage() {
const res = await fetch("/api/user");
const data = await res.json();
if (!res.ok) {
setError(data.error || "Failed to fetch user");
const message = data.error || "Failed to fetch user";
setError(message);
showToast.error("Unable to fetch data. Retry?");
return;
}
setUser(data);
} catch {
setError("Failed to fetch user data");
showToast.error("Unable to fetch data. Retry?");
} finally {
setLoading(false);
}