mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-06 21:28:25 -04:00
232 lines
6.7 KiB
TypeScript
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;
|