From bfeea743f7a930783ed70bc45080898e10464def Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 6 Feb 2026 18:40:21 -0600 Subject: [PATCH] fix(CQ-WEB-10): Add loading/error states to pages with mock data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert tasks, calendar, and dashboard pages from synchronous mock data to async loading pattern with useState/useEffect. Each page now shows a loading state via child components while data loads, and displays a PDA-friendly amber-styled message with a retry button if loading fails. This prepares these pages for real API integration by establishing the async data flow pattern. Child components (TaskList, Calendar, dashboard widgets) already handled isLoading props — now the pages actually use them. Co-Authored-By: Claude Opus 4.6 --- .../(authenticated)/calendar/page.test.tsx | 46 ++++++++++ .../src/app/(authenticated)/calendar/page.tsx | 50 +++++++++-- .../web/src/app/(authenticated)/page.test.tsx | 85 +++++++++++++++++++ apps/web/src/app/(authenticated)/page.tsx | 77 ++++++++++++----- .../app/(authenticated)/tasks/page.test.tsx | 18 +++- .../src/app/(authenticated)/tasks/page.tsx | 50 +++++++++-- 6 files changed, 284 insertions(+), 42 deletions(-) create mode 100644 apps/web/src/app/(authenticated)/calendar/page.test.tsx create mode 100644 apps/web/src/app/(authenticated)/page.test.tsx diff --git a/apps/web/src/app/(authenticated)/calendar/page.test.tsx b/apps/web/src/app/(authenticated)/calendar/page.test.tsx new file mode 100644 index 0000000..098b21a --- /dev/null +++ b/apps/web/src/app/(authenticated)/calendar/page.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import CalendarPage from "./page"; + +// Mock the Calendar component +vi.mock("@/components/calendar/Calendar", () => ({ + Calendar: ({ + events, + isLoading, + }: { + events: unknown[]; + isLoading: boolean; + }): React.JSX.Element => ( +
{isLoading ? "Loading" : `${String(events.length)} events`}
+ ), +})); + +describe("CalendarPage", (): void => { + it("should render the page title", (): void => { + render(); + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Calendar"); + }); + + it("should show loading state initially", (): void => { + render(); + expect(screen.getByTestId("calendar")).toHaveTextContent("Loading"); + }); + + it("should render the Calendar with events after loading", async (): Promise => { + render(); + await waitFor((): void => { + expect(screen.getByTestId("calendar")).toHaveTextContent("3 events"); + }); + }); + + it("should have proper layout structure", (): void => { + const { container } = render(); + const main = container.querySelector("main"); + expect(main).toBeInTheDocument(); + }); + + it("should render the subtitle text", (): void => { + render(); + expect(screen.getByText("View your schedule at a glance")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/app/(authenticated)/calendar/page.tsx b/apps/web/src/app/(authenticated)/calendar/page.tsx index d1c6d13..101231a 100644 --- a/apps/web/src/app/(authenticated)/calendar/page.tsx +++ b/apps/web/src/app/(authenticated)/calendar/page.tsx @@ -1,18 +1,39 @@ "use client"; +import { useState, useEffect } from "react"; import type { ReactElement } from "react"; import { Calendar } from "@/components/calendar/Calendar"; import { mockEvents } from "@/lib/api/events"; +import type { Event } from "@mosaic/shared"; export default function CalendarPage(): ReactElement { - // TODO: Replace with real API call when backend is ready - // const { data: events, isLoading } = useQuery({ - // queryKey: ["events"], - // queryFn: fetchEvents, - // }); + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - const events = mockEvents; - const isLoading = false; + useEffect(() => { + void loadEvents(); + }, []); + + async function loadEvents(): Promise { + setIsLoading(true); + setError(null); + + try { + // TODO: Replace with real API call when backend is ready + // const data = await fetchEvents(); + await new Promise((resolve) => setTimeout(resolve, 300)); + setEvents(mockEvents); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "We had trouble loading your calendar. Please try again when you're ready." + ); + } finally { + setIsLoading(false); + } + } return (
@@ -20,7 +41,20 @@ export default function CalendarPage(): ReactElement {

Calendar

View your schedule at a glance

- + + {error !== null ? ( +
+

{error}

+ +
+ ) : ( + + )}
); } diff --git a/apps/web/src/app/(authenticated)/page.test.tsx b/apps/web/src/app/(authenticated)/page.test.tsx new file mode 100644 index 0000000..4702f0d --- /dev/null +++ b/apps/web/src/app/(authenticated)/page.test.tsx @@ -0,0 +1,85 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import DashboardPage from "./page"; + +// Mock dashboard widgets +vi.mock("@/components/dashboard/RecentTasksWidget", () => ({ + RecentTasksWidget: ({ + tasks, + isLoading, + }: { + tasks: unknown[]; + isLoading: boolean; + }): React.JSX.Element => ( +
+ {isLoading ? "Loading tasks" : `${String(tasks.length)} tasks`} +
+ ), +})); + +vi.mock("@/components/dashboard/UpcomingEventsWidget", () => ({ + UpcomingEventsWidget: ({ + events, + isLoading, + }: { + events: unknown[]; + isLoading: boolean; + }): React.JSX.Element => ( +
+ {isLoading ? "Loading events" : `${String(events.length)} events`} +
+ ), +})); + +vi.mock("@/components/dashboard/QuickCaptureWidget", () => ({ + QuickCaptureWidget: (): React.JSX.Element =>
Quick Capture
, +})); + +vi.mock("@/components/dashboard/DomainOverviewWidget", () => ({ + DomainOverviewWidget: ({ + tasks, + isLoading, + }: { + tasks: unknown[]; + isLoading: boolean; + }): React.JSX.Element => ( +
+ {isLoading ? "Loading overview" : `${String(tasks.length)} tasks overview`} +
+ ), +})); + +describe("DashboardPage", (): void => { + it("should render the page title", (): void => { + render(); + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Dashboard"); + }); + + it("should show loading state initially", (): void => { + render(); + expect(screen.getByTestId("recent-tasks")).toHaveTextContent("Loading tasks"); + expect(screen.getByTestId("upcoming-events")).toHaveTextContent("Loading events"); + expect(screen.getByTestId("domain-overview")).toHaveTextContent("Loading overview"); + }); + + it("should render all widgets with data after loading", async (): Promise => { + render(); + await waitFor((): void => { + expect(screen.getByTestId("recent-tasks")).toHaveTextContent("4 tasks"); + expect(screen.getByTestId("upcoming-events")).toHaveTextContent("3 events"); + expect(screen.getByTestId("domain-overview")).toHaveTextContent("4 tasks overview"); + expect(screen.getByTestId("quick-capture")).toBeInTheDocument(); + }); + }); + + it("should have proper layout structure", (): void => { + const { container } = render(); + const main = container.querySelector("main"); + expect(main).toBeInTheDocument(); + }); + + it("should render the welcome subtitle", (): void => { + render(); + expect(screen.getByText(/Welcome back/)).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/app/(authenticated)/page.tsx b/apps/web/src/app/(authenticated)/page.tsx index 532c87d..8d637f7 100644 --- a/apps/web/src/app/(authenticated)/page.tsx +++ b/apps/web/src/app/(authenticated)/page.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { useState, useEffect } from "react"; import type { ReactElement } from "react"; import { RecentTasksWidget } from "@/components/dashboard/RecentTasksWidget"; import { UpcomingEventsWidget } from "@/components/dashboard/UpcomingEventsWidget"; @@ -5,43 +8,71 @@ import { QuickCaptureWidget } from "@/components/dashboard/QuickCaptureWidget"; import { DomainOverviewWidget } from "@/components/dashboard/DomainOverviewWidget"; import { mockTasks } from "@/lib/api/tasks"; import { mockEvents } from "@/lib/api/events"; +import type { Task, Event } from "@mosaic/shared"; export default function DashboardPage(): ReactElement { - // TODO: Replace with real API call when backend is ready - // const { data: tasks, isLoading: tasksLoading } = useQuery({ - // queryKey: ["tasks"], - // queryFn: fetchTasks, - // }); - // const { data: events, isLoading: eventsLoading } = useQuery({ - // queryKey: ["events"], - // queryFn: fetchEvents, - // }); + const [tasks, setTasks] = useState([]); + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - const tasks = mockTasks; - const events = mockEvents; - const tasksLoading = false; - const eventsLoading = false; + useEffect(() => { + void loadDashboardData(); + }, []); + + async function loadDashboardData(): Promise { + setIsLoading(true); + setError(null); + + try { + // TODO: Replace with real API calls when backend is ready + // const [tasksData, eventsData] = await Promise.all([fetchTasks(), fetchEvents()]); + await new Promise((resolve) => setTimeout(resolve, 300)); + setTasks(mockTasks); + setEvents(mockEvents); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "We had trouble loading your dashboard. Please try again when you're ready." + ); + } finally { + setIsLoading(false); + } + } return (

Dashboard

-

Welcome back! Here's your overview

+

Welcome back! Here's your overview

-
- {/* Top row: Domain Overview and Quick Capture */} -
- + {error !== null ? ( +
+

{error}

+
+ ) : ( +
+ {/* Top row: Domain Overview and Quick Capture */} +
+ +
- - + + -
- +
+ +
-
+ )}
); } diff --git a/apps/web/src/app/(authenticated)/tasks/page.test.tsx b/apps/web/src/app/(authenticated)/tasks/page.test.tsx index a317f18..a0c9966 100644 --- a/apps/web/src/app/(authenticated)/tasks/page.test.tsx +++ b/apps/web/src/app/(authenticated)/tasks/page.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import TasksPage from "./page"; // Mock the TaskList component @@ -15,9 +15,16 @@ describe("TasksPage", (): void => { expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Tasks"); }); - it("should render the TaskList component", (): void => { + it("should show loading state initially", (): void => { render(); - expect(screen.getByTestId("task-list")).toBeInTheDocument(); + expect(screen.getByTestId("task-list")).toHaveTextContent("Loading"); + }); + + it("should render the TaskList with tasks after loading", async (): Promise => { + render(); + await waitFor((): void => { + expect(screen.getByTestId("task-list")).toHaveTextContent("4 tasks"); + }); }); it("should have proper layout structure", (): void => { @@ -25,4 +32,9 @@ describe("TasksPage", (): void => { const main = container.querySelector("main"); expect(main).toBeInTheDocument(); }); + + it("should render the subtitle text", (): void => { + render(); + expect(screen.getByText("Organize your work at your own pace")).toBeInTheDocument(); + }); }); diff --git a/apps/web/src/app/(authenticated)/tasks/page.tsx b/apps/web/src/app/(authenticated)/tasks/page.tsx index 373409b..6873ce1 100644 --- a/apps/web/src/app/(authenticated)/tasks/page.tsx +++ b/apps/web/src/app/(authenticated)/tasks/page.tsx @@ -1,19 +1,40 @@ "use client"; +import { useState, useEffect } from "react"; import type { ReactElement } from "react"; import { TaskList } from "@/components/tasks/TaskList"; import { mockTasks } from "@/lib/api/tasks"; +import type { Task } from "@mosaic/shared"; export default function TasksPage(): ReactElement { - // TODO: Replace with real API call when backend is ready - // const { data: tasks, isLoading } = useQuery({ - // queryKey: ["tasks"], - // queryFn: fetchTasks, - // }); + const [tasks, setTasks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - const tasks = mockTasks; - const isLoading = false; + useEffect(() => { + void loadTasks(); + }, []); + + async function loadTasks(): Promise { + setIsLoading(true); + setError(null); + + try { + // TODO: Replace with real API call when backend is ready + // const data = await fetchTasks(); + await new Promise((resolve) => setTimeout(resolve, 300)); + setTasks(mockTasks); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "We had trouble loading your tasks. Please try again when you're ready." + ); + } finally { + setIsLoading(false); + } + } return (
@@ -21,7 +42,20 @@ export default function TasksPage(): ReactElement {

Tasks

Organize your work at your own pace

- + + {error !== null ? ( +
+

{error}

+ +
+ ) : ( + + )}
); }