Cleaning up, improving the landing page.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-28 09:50:22 -04:00
parent e4dd729f86
commit aa056d4dfc
4 changed files with 145 additions and 27 deletions
@@ -16,6 +16,13 @@ describe("RoomList", () => {
equipment: "Projector",
capacity: 10,
},
{
id: 2,
name: "Room B",
location: "Building 2",
equipment: "Whiteboard",
capacity: 8,
},
];
test("renders rooms with correct details", () => {
@@ -24,6 +31,10 @@ describe("RoomList", () => {
expect(screen.getByText(/Location: Building 1/)).toBeInTheDocument();
expect(screen.getByText(/Capacity: 10/)).toBeInTheDocument();
expect(screen.getByText(/Equipment: Projector/)).toBeInTheDocument();
expect(screen.getByText("Room B")).toBeInTheDocument();
expect(screen.getByText(/Location: Building 2/)).toBeInTheDocument();
expect(screen.getByText(/Capacity: 8/)).toBeInTheDocument();
expect(screen.getByText(/Equipment: Whiteboard/)).toBeInTheDocument();
expect(
screen.getByRole("list", { name: /available conference rooms/i })
).toBeInTheDocument();
@@ -33,4 +44,29 @@ describe("RoomList", () => {
render(<RoomList rooms={[]} />);
expect(screen.getByText("No rooms available.")).toBeInTheDocument();
});
test("calls onSelectRoom when a room is clicked", () => {
const onSelectRoom = jest.fn();
render(
<RoomList
rooms={mockRooms}
selectedRoomId={2}
onSelectRoom={onSelectRoom}
/>
);
const roomA = screen.getByTestId("room-item-1");
const roomB = screen.getByTestId("room-item-2");
roomA.click();
expect(onSelectRoom).toHaveBeenCalledWith(1);
roomB.click();
expect(onSelectRoom).toHaveBeenCalledWith(2);
});
test("highlights the selected room", () => {
render(
<RoomList rooms={mockRooms} selectedRoomId={2} onSelectRoom={() => {}} />
);
const roomB = screen.getByTestId("room-item-2");
expect(roomB.className).toMatch(/Mui-selected/);
});
});
@@ -5,7 +5,7 @@
* Tests integration of RoomList and BookingList, API data fetching, loading states,
* error handling, and navigation. Mocks backend endpoints:
* - GET `/rooms/`: Returns ConferenceRoom[].
* - GET `/bookings/room/?date={YYYY-MM-DD}`: Returns Booking[].
* - GET `/bookings/room/{roomId}?date={YYYY-MM-DD}`: Returns Booking[] for the selected room and date.
*/
import { render, screen, waitFor } from "@testing-library/react";
@@ -40,14 +40,30 @@ describe("LandingPage", () => {
equipment: "Projector",
capacity: 10,
},
{
id: 2,
name: "Room B",
location: "Building 2",
equipment: "Whiteboard",
capacity: 8,
},
];
const mockBookings: Booking[] = [
const mockBookingsA: Booking[] = [
{
id: 1,
roomId: 1,
startTime: "2025-08-27T10:00:00Z",
endTime: "2025-08-27T11:00:00Z",
title: "Meeting",
title: "Meeting A",
},
];
const mockBookingsB: Booking[] = [
{
id: 2,
roomId: 2,
startTime: "2025-08-27T12:00:00Z",
endTime: "2025-08-27T13:00:00Z",
title: "Meeting B",
},
];
@@ -63,20 +79,30 @@ describe("LandingPage", () => {
expect(screen.getAllByTestId("skeleton-loader").length).toBeGreaterThan(0);
});
test("renders rooms and bookings correctly", async () => {
test("renders rooms and bookings correctly, and updates bookings on room selection", async () => {
mockedAxios.get
.mockResolvedValueOnce({ data: mockRooms })
.mockResolvedValueOnce({ data: mockBookings });
.mockResolvedValueOnce({ data: mockRooms }) // rooms
.mockResolvedValueOnce({ data: mockBookingsA }) // bookings for Room A
.mockResolvedValueOnce({ data: mockBookingsB }); // bookings for Room B
render(<LandingPage />, {
wrapper: ({ children }) =>
createWrapper(<MemoryRouter>{children}</MemoryRouter>),
});
await waitFor(() => {
expect(screen.getByText("Room A")).toBeInTheDocument();
});
expect(screen.getByText("Meeting")).toBeInTheDocument();
// Wait for rooms to load
await screen.findByText("Room A");
await screen.findByText("Room B");
// Wait for bookings for Room A
await screen.findByText("Meeting A");
// Select Room B
const roomB = screen.getByTestId("room-item-2");
roomB.click();
// Wait for bookings for Room B
await screen.findByText("Meeting B");
expect(
screen.getByRole("button", { name: /navigate to booking page/i })
).toBeInTheDocument();
+48 -10
View File
@@ -1,10 +1,16 @@
/**
* Displays a list of available conference rooms in a Material-UI Card.
*
* Rooms are selectable if `onSelectRoom` is provided. The selected room is visually highlighted.
*
* @component
* @example
* ```tsx
* <RoomList rooms={[{ id: 1, name: 'Room A', location: 'Building 1', equipment: 'Projector', capacity: 10 }]} />
* <RoomList
* rooms={[{ id: 1, name: 'Room A', location: 'Building 1', equipment: 'Projector', capacity: 10 }]}
* selectedRoomId={1}
* onSelectRoom={roomId => setSelectedRoomId(roomId)}
* />
* ```
*/
import {
@@ -14,6 +20,7 @@ import {
ListItem,
ListItemText,
Typography,
ListItemButton,
} from "@mui/material";
import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
import { FC } from "react";
@@ -35,8 +42,16 @@ export interface ConferenceRoom {
capacity: number;
}
/**
* Props for RoomList.
* @property {ConferenceRoom[]} rooms - List of rooms to display.
* @property {number} [selectedRoomId] - The currently selected room's id.
* @property {(roomId: number) => void} [onSelectRoom] - Callback when a room is selected.
*/
interface RoomListProps {
rooms: ConferenceRoom[];
selectedRoomId?: number;
onSelectRoom?: (roomId: number) => void;
}
/**
@@ -44,7 +59,11 @@ interface RoomListProps {
* @param {RoomListProps} props - Component props.
* @returns {JSX.Element} A Card containing a list of rooms.
*/
const RoomList: FC<RoomListProps> = ({ rooms }) => {
const RoomList: FC<RoomListProps> = ({
rooms,
selectedRoomId,
onSelectRoom,
}) => {
return (
<Card>
<CardContent>
@@ -54,14 +73,33 @@ const RoomList: FC<RoomListProps> = ({ rooms }) => {
</Typography>
<List aria-label="Available conference rooms">
{rooms.length > 0 ? (
rooms.map((room) => (
<ListItem key={room.id}>
<ListItemText
primary={room.name}
secondary={`Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`}
/>
</ListItem>
))
rooms.map((room) =>
onSelectRoom ? (
<ListItemButton
key={room.id}
selected={room.id === selectedRoomId}
onClick={() => onSelectRoom(room.id)}
aria-label={`Select room ${room.name}`}
data-testid={`room-item-${room.id}`}
>
<ListItemText
primary={room.name}
secondary={`Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`}
/>
</ListItemButton>
) : (
<ListItem
key={room.id}
aria-label={`Select room ${room.name}`}
data-testid={`room-item-${room.id}`}
>
<ListItemText
primary={room.name}
secondary={`Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`}
/>
</ListItem>
)
)
) : (
<Typography>No rooms available.</Typography>
)}
+25 -7
View File
@@ -20,13 +20,16 @@ import {
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { FC } from "react";
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 = () => {
@@ -45,19 +48,30 @@ const LandingPage: FC = () => {
axios.get("/rooms/").then((res) => res.data as ConferenceRoom[]),
});
// Fetch today's bookings for the first room (if any)
const firstRoomId = rooms.length > 0 ? rooms[0].id : undefined;
// 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, firstRoomId],
enabled: !!firstRoomId,
queryKey: ["bookings", today, selectedRoomId],
enabled: !!selectedRoomId,
queryFn: () =>
axios
.get(`/bookings/room/${firstRoomId}?date=${today}`)
.get(`/bookings/room/${selectedRoomId}?date=${today}`)
.then((res) => res.data as Booking[]),
});
@@ -143,7 +157,11 @@ const LandingPage: FC = () => {
</Typography>
<Grid container spacing={4}>
<Grid size={{ xs: 12, md: 6 }}>
<RoomList rooms={rooms} />
<RoomList
rooms={rooms}
selectedRoomId={selectedRoomId}
onSelectRoom={setSelectedRoomId}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<BookingList bookings={bookings} />