Files
conference-room-booking-system/frontend/src/pages/LandingPage.tsx
2025-08-28 16:24:32 -04:00

232 lines
6.7 KiB
TypeScript

/**
* Main entry point for the conference room booking system.
* Displays available rooms, today's bookings, and a navigation button to the booking page.
*
* @remarks
* Fetches data from:
* - GET `/rooms/`: Retrieves available rooms.
* - GET `/bookings/room/?date={YYYY-MM-DD}`: Retrieves bookings for the current day.
*
* @component
*/
import { Typography, Button, Skeleton, Box } from "@mui/material";
import "./LandingPage.css";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { FC, useEffect, useState } from "react";
import RoomList, { ConferenceRoom } from "../components/RoomList";
import BookingList, { Booking } from "../components/BookingList";
/**
* Renders the landing page with rooms, bookings, and navigation.
* Uses react-query for data fetching and skeleton loaders for loading states.
*
* Rooms are selectable; selecting a room refreshes the bookings list for that room. The first room is selected by default.
*
* @returns {JSX.Element} The landing page UI.
*/
const LandingPage: FC = () => {
// Debug: log when LandingPage mounts
useEffect(() => {
console.log("LandingPage: mounted");
return () => {
console.log("LandingPage: unmounted");
};
}, []);
// Debug: log before page unload
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
console.log("window.onbeforeunload: page is unloading");
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, []);
const navigate = useNavigate();
const today = new Date().toISOString().split("T")[0];
// Fetch rooms
const {
data: rooms = [],
isLoading: roomsLoading,
error: roomsError,
refetch: refetchRooms,
} = useQuery({
queryKey: ["rooms"],
queryFn: () =>
axios.get("/rooms/").then((res) => res.data as ConferenceRoom[]),
});
// Room selection state
const [selectedRoomId, setSelectedRoomId] = useState<number | undefined>(
undefined
);
// Default to first room when rooms load
useEffect(() => {
if (rooms.length > 0 && selectedRoomId === undefined) {
setSelectedRoomId(rooms[0].id);
}
}, [rooms, selectedRoomId]);
// Fetch bookings for selected room
const {
data: bookings = [],
isLoading: bookingsLoading,
error: bookingsError,
refetch: refetchBookings,
} = useQuery({
queryKey: ["bookings", today, selectedRoomId],
enabled: !!selectedRoomId,
queryFn: () => {
console.log("LandingPage: fetching bookings for", selectedRoomId, today);
return axios
.get(`/bookings/room/${selectedRoomId}?date=${today}`)
.then((res) => {
console.log("LandingPage: bookings fetch result", res.data);
return res.data as Booking[];
})
.catch((err) => {
console.error("LandingPage: bookings fetch error", err);
throw err;
});
},
});
// Display skeleton loaders during data fetching
if (roomsLoading || bookingsLoading) {
return (
<div className="landing-root">
<div className="landing-section">
<Box sx={{ p: 2 }}>
<Skeleton
variant="text"
width="50%"
data-testid="skeleton-loader"
/>
{[...Array(3)].map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
height={60}
sx={{ mb: 1 }}
data-testid="skeleton-loader"
/>
))}
</Box>
</div>
<div className="landing-section">
<Box sx={{ p: 2 }}>
<Skeleton
variant="text"
width="50%"
data-testid="skeleton-loader"
/>
{[...Array(3)].map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
height={60}
sx={{ mb: 1 }}
data-testid="skeleton-loader"
/>
))}
</Box>
</div>
</div>
);
}
// Display error message with retry option
if (roomsError || bookingsError) {
console.error("LandingPage: error state", { roomsError, bookingsError });
return (
<div
className="landing-root"
style={{ alignItems: "center", justifyContent: "center" }}
>
<div style={{ width: "100%", textAlign: "center" }}>
<Typography color="error" data-testid="error-message">
{roomsError?.message ||
bookingsError?.message ||
"Failed to fetch data."}
</Typography>
<Button
variant="contained"
onClick={() => {
refetchRooms();
refetchBookings();
}}
sx={{ mt: 2 }}
aria-label="Retry fetching data"
>
Retry
</Button>
</div>
</div>
);
}
return (
<div className="landing-root">
<header className="landing-header">
<Typography
variant="h4"
gutterBottom
align="center"
sx={{ fontSize: { xs: "1.5rem", md: "2.25rem" } }}
>
Conference Room Booking System
</Typography>
</header>
<main className="landing-main">
<div className="landing-columns-row">
<div className="landing-section landing-card landing-card-dark">
<RoomList
rooms={rooms}
selectedRoomId={selectedRoomId}
onSelectRoom={setSelectedRoomId}
/>
</div>
<div className="landing-section landing-card landing-card-dark">
<BookingList
bookings={bookings}
roomId={selectedRoomId}
date={today}
/>
</div>
</div>
{/* Desktop/landscape Book a Room button and HR */}
<div className="landing-book-btn-desktop-wide">
<hr className="landing-hr-wide" />
<Button
variant="contained"
color="primary"
onClick={() => navigate("/booking")}
aria-label="Navigate to booking page"
data-testid="desktop-book-btn"
>
Book a Room
</Button>
</div>
</main>
{/* Mobile/portrait Book a Room button and HR */}
<div className="landing-book-btn-mobile">
<hr className="landing-hr" />
<Button
variant="contained"
color="primary"
onClick={() => navigate("/booking")}
aria-label="Navigate to booking page"
data-testid="mobile-book-btn"
>
Book a Room
</Button>
</div>
</div>
);
};
export default LandingPage;