From ae1e0f812920317f63aff6ac0ff5833a0c996165 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto Date: Sun, 30 Aug 2026 04:15:37 +0000 Subject: [PATCH] fix(frontend): wire passenger and staff pages to the API/hooks layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/api.ts and lib/hooks/useApi.ts implement exactly the "real API with mock fallback" behavior CLAUDE.md documents, but no page ever called them — app/page.tsx, app/staff/page.tsx and LostItemModal each duplicated their own setTimeout-based mock simulation instead, so the hooks layer was dead code and the app never attempted a real request. Wire all three to the existing hooks (useCurrentTrip, useTrips, useReportLostItem, useDriverNotificationsApi) so the documented fallback path actually runs. This also surfaced a latent bug in useDriverNotificationsApi: updateNotification's optimistic setLocalData bailed out whenever localData was still null (its initial value, since nothing populated it before this), silently no-oping the first status update. Fixed by falling back to the fetched/mock data as the base to update. Verified via a headless-browser pass of both pages, including the report-lost and found/not-found flows, with dev talking to the configured (but unreachable) backend and falling back to mock data as intended. --- frontend/app/page.tsx | 23 +++----- frontend/app/staff/page.tsx | 58 ++++++++++--------- .../components/passenger/LostItemModal.tsx | 20 ++++--- frontend/lib/hooks/useApi.ts | 15 +++-- frontend/lib/mock-data.ts | 3 - 5 files changed, 58 insertions(+), 61 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 9ee2335..070f925 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,14 +1,15 @@ 'use client'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { StatusBar } from '@/components/ui/StatusBar'; import { Header } from '@/components/passenger/Header'; import { TripCard } from '@/components/passenger/TripCard'; import { LostItemModal } from '@/components/passenger/LostItemModal'; import { BottomNav, type NavTab } from '@/components/ui/BottomNav'; import { Toast } from '@/components/ui/Toast'; -import { mockUser, mockTrips, mockActiveTrip, formatRelativeTime } from '@/lib/mock-data'; +import { mockUser, formatRelativeTime } from '@/lib/mock-data'; import { config } from '@/lib/config'; +import { useCurrentTrip, useTrips } from '@/lib/hooks'; import { UI_LABELS } from '@/lib/labels'; import type { Trip, LostItem } from '@/lib/types'; @@ -177,20 +178,10 @@ export default function PassengerApp() { type: 'success' | 'error' | 'info'; } | null>(null); const [activeTab, setActiveTab] = useState('reisen'); - const [isLoading, setIsLoading] = useState(true); - const [currentTrip, setCurrentTrip] = useState(null); - const [recentTrips, setRecentTrips] = useState([]); - - // Load trips data (simulates API call) - useEffect(() => { - const loadData = async () => { - await new Promise((resolve) => setTimeout(resolve, config.demo.mockDelay)); - setCurrentTrip(mockActiveTrip); - setRecentTrips(mockTrips); - setIsLoading(false); - }; - loadData(); - }, []); + const { data: currentTrip, isLoading: isLoadingCurrentTrip } = useCurrentTrip(); + const { data: recentTripsData, isLoading: isLoadingTrips } = useTrips(); + const recentTrips = recentTripsData ?? []; + const isLoading = isLoadingCurrentTrip || isLoadingTrips; const handleReportLost = useCallback((trip: Trip) => { setSelectedTrip(trip); diff --git a/frontend/app/staff/page.tsx b/frontend/app/staff/page.tsx index 22d1312..d53a107 100644 --- a/frontend/app/staff/page.tsx +++ b/frontend/app/staff/page.tsx @@ -5,25 +5,24 @@ import { StaffHeader } from '@/components/staff/StaffHeader'; import { NotificationCard } from '@/components/staff/NotificationCard'; import { StaffStatusBar } from '@/components/staff/StaffStatusBar'; import type { StaffNotification, NotificationStatus } from '@/lib/types'; -import { mockStaffNotifications, mockStaff, mockVehicle } from '@/lib/mock-data'; +import { mockStaff, mockVehicle } from '@/lib/mock-data'; import { config } from '@/lib/config'; +import { useDriverNotificationsApi } from '@/lib/hooks'; import { UI_LABELS } from '@/lib/labels'; export default function StaffPage() { - const [notifications, setNotifications] = useState([]); - const [isLoading, setIsLoading] = useState(true); + // Injected demo notification, prepended ahead of the fetched/mock list below. + const [demoNotifications, setDemoNotifications] = useState([]); const [activeFilter, setActiveFilter] = useState<'all' | 'pending' | 'resolved'>('all'); const [showNewNotification, setShowNewNotification] = useState(false); - useEffect(() => { - // Simulate loading notifications - const timer = setTimeout(() => { - setNotifications(mockStaffNotifications); - setIsLoading(false); - }, config.demo.mockDelay); + const { + data: fetchedNotifications, + isLoading, + updateNotification, + } = useDriverNotificationsApi(mockVehicle.id); - return () => clearTimeout(timer); - }, []); + const notifications = [...demoNotifications, ...(fetchedNotifications ?? [])]; // Simulate incoming notification for demo useEffect(() => { @@ -50,7 +49,7 @@ export default function StaffPage() { }, }; - setNotifications((prev) => [newNotification, ...prev]); + setDemoNotifications((prev) => [newNotification, ...prev]); // Play notification sound (if available) if (typeof window !== 'undefined' && 'vibrate' in navigator) { @@ -63,23 +62,26 @@ export default function StaffPage() { const handleUpdateStatus = useCallback( async (notificationId: string, status: NotificationStatus, notes?: string) => { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, config.demo.mockDelay)); - - setNotifications((prev) => - prev.map((n) => - n.id === notificationId - ? { - ...n, - status, - respondedAt: new Date().toISOString(), - response: notes ? { notes, foundItem: status === 'found' } : undefined, - } - : n, - ), - ); + if (demoNotifications.some((n) => n.id === notificationId)) { + setDemoNotifications((prev) => + prev.map((n) => + n.id === notificationId + ? { + ...n, + status, + respondedAt: new Date().toISOString(), + response: notes ? { notes, foundItem: status === 'found' } : undefined, + } + : n, + ), + ); + return; + } + + if (status !== 'found' && status !== 'not_found') return; + await updateNotification(notificationId, status, notes); }, - [], + [demoNotifications, updateNotification], ); const filteredNotifications = notifications.filter((n) => { diff --git a/frontend/components/passenger/LostItemModal.tsx b/frontend/components/passenger/LostItemModal.tsx index cc02d07..8154316 100644 --- a/frontend/components/passenger/LostItemModal.tsx +++ b/frontend/components/passenger/LostItemModal.tsx @@ -10,6 +10,7 @@ import { } from '@/lib/types'; import { formatTime, getTimeSinceTrip } from '@/lib/mock-data'; import { config } from '@/lib/config'; +import { useReportLostItem } from '@/lib/hooks'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; interface LostItemModalProps { @@ -25,19 +26,23 @@ export function LostItemModal({ trip, onClose, onSubmit }: LostItemModalProps) { const [category, setCategory] = useState(null); const [description, setDescription] = useState(''); const [location, setLocation] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); + const { reportItem, isSubmitting } = useReportLostItem(); const { minutes, isUrgent } = getTimeSinceTrip(trip.arrivalTime); const handleSubmit = useCallback(async () => { if (!category || !description.trim()) return; - setIsSubmitting(true); - - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, config.demo.mockDelay)); + const result = await reportItem({ + tripId: trip.id, + category, + description, + location: location || 'unknown', + }); - const newItem: LostItem = { + // No backend configured (or it errored): fall back to a locally built + // item so the demo flow still completes. + const newItem: LostItem = result.item ?? { id: `lost-${Date.now()}`, userId: 'user-001', tripId: trip.id, @@ -49,14 +54,13 @@ export function LostItemModal({ trip, onClose, onSubmit }: LostItemModalProps) { updatedAt: new Date().toISOString(), }; - setIsSubmitting(false); setStep('success'); // Notify parent after showing success setTimeout(() => { onSubmit(newItem); }, config.timing.successMessageDelay); - }, [category, description, location, trip.id, onSubmit]); + }, [category, description, location, trip.id, onSubmit, reportItem]); const handleSelectCategory = (cat: ItemCategory) => { setCategory(cat); diff --git a/frontend/lib/hooks/useApi.ts b/frontend/lib/hooks/useApi.ts index fa95bbc..19b6843 100644 --- a/frontend/lib/hooks/useApi.ts +++ b/frontend/lib/hooks/useApi.ts @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react'; import { api } from '../api'; import { config } from '../config'; import type { Trip, DriverNotification, LostItem } from '../types'; -import { mockTrips, mockDriverNotifications, mockActiveTrip } from '../mock-data'; +import { mockTrips, mockStaffNotifications, mockActiveTrip } from '../mock-data'; interface UseApiState { data: T | null; @@ -107,7 +107,7 @@ export function useDriverNotificationsApi( const response = await api.getDriverNotifications(vehicleId, filter); return response; }, - mockDriverNotifications, + mockStaffNotifications, [vehicleId, filter], ); @@ -116,10 +116,13 @@ export function useDriverNotificationsApi( const updateNotification = useCallback( async (id: string, status: 'found' | 'not_found', notes?: string) => { - // Optimistic update + // Optimistic update. `prev` is localData, which starts out null until this + // runs once — fall back to the fetched/mock list so the first response + // actually applies instead of updating nothing. setLocalData((prev) => { - if (!prev) return prev; - return prev.map((n) => + const base = prev ?? baseState.data; + if (!base) return prev; + return base.map((n) => n.id === id ? { ...n, @@ -138,7 +141,7 @@ export function useDriverNotificationsApi( console.log('API update failed, keeping local state'); } }, - [], + [baseState.data], ); return { diff --git a/frontend/lib/mock-data.ts b/frontend/lib/mock-data.ts index bc6288f..1e1f671 100644 --- a/frontend/lib/mock-data.ts +++ b/frontend/lib/mock-data.ts @@ -384,6 +384,3 @@ export const mockStaffNotifications: StaffNotification[] = [ }, }, ]; - -/** @deprecated Use mockStaffNotifications instead */ -export const mockDriverNotifications = mockStaffNotifications;