Public Access
mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-11 10:28:45 -04:00
Cleaning up, improving the landing page.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user