From f97be2e6a311b39d61a449a4d9fa78683d61b57c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 23 Feb 2026 03:51:15 +0000 Subject: [PATCH] feat(web): wire calendar page to real API data (#474) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../src/app/(authenticated)/calendar/page.tsx | 168 ++++++++++++++---- apps/web/src/lib/api/events.ts | 97 ++++------ 2 files changed, 168 insertions(+), 97 deletions(-) diff --git a/apps/web/src/app/(authenticated)/calendar/page.tsx b/apps/web/src/app/(authenticated)/calendar/page.tsx index 101231a..4005691 100644 --- a/apps/web/src/app/(authenticated)/calendar/page.tsx +++ b/apps/web/src/app/(authenticated)/calendar/page.tsx @@ -3,57 +3,161 @@ import { useState, useEffect } from "react"; import type { ReactElement } from "react"; import { Calendar } from "@/components/calendar/Calendar"; -import { mockEvents } from "@/lib/api/events"; +import { fetchEvents } from "@/lib/api/events"; +import { MosaicSpinner } from "@/components/ui/MosaicSpinner"; +import { useWorkspaceId } from "@/lib/hooks"; import type { Event } from "@mosaic/shared"; export default function CalendarPage(): ReactElement { + const workspaceId = useWorkspaceId(); const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); 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 { + if (!workspaceId) { setIsLoading(false); + return; } + + const wsId = workspaceId; + let cancelled = false; + setError(null); + setIsLoading(true); + + async function loadEvents(): Promise { + try { + const data = await fetchEvents(wsId); + if (!cancelled) { + setEvents(data); + } + } catch (err: unknown) { + console.error("[Calendar] Failed to fetch events:", err); + if (!cancelled) { + setError( + err instanceof Error + ? err.message + : "We had trouble loading your calendar. Please try again when you're ready." + ); + } + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + } + + void loadEvents(); + + return (): void => { + cancelled = true; + }; + }, [workspaceId]); + + function handleRetry(): void { + if (!workspaceId) return; + + const wsId = workspaceId; + setError(null); + setIsLoading(true); + + fetchEvents(wsId) + .then((data) => { + setEvents(data); + }) + .catch((err: unknown) => { + console.error("[Calendar] Retry failed:", err); + setError( + err instanceof Error + ? err.message + : "We had trouble loading your calendar. Please try again when you're ready." + ); + }) + .finally(() => { + setIsLoading(false); + }); + } + + if (isLoading) { + return ( +
+
+

+ Calendar +

+

+ View your schedule at a glance +

+
+
+ +
+
+ ); + } + + if (error !== null) { + return ( +
+
+

+ Calendar +

+

+ View your schedule at a glance +

+
+
+

{error}

+ +
+
+ ); } return (
-

Calendar

-

View your schedule at a glance

+

+ Calendar +

+

+ View your schedule at a glance +

- {error !== null ? ( -
-

{error}

- + {events.length === 0 ? ( +
+

+ No events scheduled +

+

+ Your calendar is clear +

) : ( - + )}
); diff --git a/apps/web/src/lib/api/events.ts b/apps/web/src/lib/api/events.ts index b85d88e..f333526 100644 --- a/apps/web/src/lib/api/events.ts +++ b/apps/web/src/lib/api/events.ts @@ -7,84 +7,51 @@ import type { Event } from "@mosaic/shared"; import { apiGet, type ApiResponse } from "./client"; export interface EventFilters { - startDate?: Date; - endDate?: Date; - workspaceId?: string; + /** Filter events starting from this date (inclusive) */ + startFrom?: Date; + /** Filter events starting up to this date (inclusive) */ + startTo?: Date; + /** Filter by project ID */ + projectId?: string; + /** Filter by all-day events */ + allDay?: boolean; + /** Page number (1-based) */ + page?: number; + /** Items per page (max 100) */ + limit?: number; } /** * Fetch events with optional filters + * + * @param workspaceId - Workspace ID sent via X-Workspace-Id header + * @param filters - Optional query parameter filters */ -export async function fetchEvents(filters?: EventFilters): Promise { +export async function fetchEvents(workspaceId?: string, filters?: EventFilters): Promise { const params = new URLSearchParams(); - if (filters?.startDate) { - params.append("startDate", filters.startDate.toISOString()); + if (filters?.startFrom) { + params.append("startFrom", filters.startFrom.toISOString()); } - if (filters?.endDate) { - params.append("endDate", filters.endDate.toISOString()); + if (filters?.startTo) { + params.append("startTo", filters.startTo.toISOString()); } - if (filters?.workspaceId) { - params.append("workspaceId", filters.workspaceId); + if (filters?.projectId) { + params.append("projectId", filters.projectId); + } + if (filters?.allDay !== undefined) { + params.append("allDay", String(filters.allDay)); + } + if (filters?.page !== undefined) { + params.append("page", String(filters.page)); + } + if (filters?.limit !== undefined) { + params.append("limit", String(filters.limit)); } const queryString = params.toString(); const endpoint = queryString ? `/api/events?${queryString}` : "/api/events"; - const response = await apiGet>(endpoint); + const response = await apiGet>(endpoint, workspaceId); return response.data; } - -/** - * Mock events for development (until backend endpoints are ready) - */ -export const mockEvents: Event[] = [ - { - id: "event-1", - title: "Team standup", - description: "Daily sync meeting", - startTime: new Date("2026-01-29T10:00:00"), - endTime: new Date("2026-01-29T10:30:00"), - allDay: false, - location: "Zoom", - recurrence: null, - creatorId: "user-1", - workspaceId: "workspace-1", - projectId: null, - metadata: {}, - createdAt: new Date("2026-01-28"), - updatedAt: new Date("2026-01-28"), - }, - { - id: "event-2", - title: "Project review", - description: "Quarterly project review session", - startTime: new Date("2026-01-30T14:00:00"), - endTime: new Date("2026-01-30T15:30:00"), - allDay: false, - location: "Conference Room A", - recurrence: null, - creatorId: "user-1", - workspaceId: "workspace-1", - projectId: null, - metadata: {}, - createdAt: new Date("2026-01-28"), - updatedAt: new Date("2026-01-28"), - }, - { - id: "event-3", - title: "Focus time", - description: "Dedicated time for deep work", - startTime: new Date("2026-01-31T09:00:00"), - endTime: new Date("2026-01-31T12:00:00"), - allDay: false, - location: null, - recurrence: null, - creatorId: "user-1", - workspaceId: "workspace-1", - projectId: null, - metadata: {}, - createdAt: new Date("2026-01-28"), - updatedAt: new Date("2026-01-28"), - }, -];