Working on the landing page.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-27 18:17:25 -04:00
parent 1073d53520
commit 04c012e9ab
3 changed files with 1184 additions and 180 deletions

View File

@@ -7,20 +7,22 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.1",
"@mui/material": "^7.3.1",
"@tanstack/react-query": "^5.85.5",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/jest": "^30.0.0",
"@types/node": "^16.7.13",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.8",
"@types/react-router-dom": "^5.3.3",
"axios": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"react-scripts": "5.0.1",
"typescript": "^4.4.2",
"typescript": "^5.9.2",
"web-vitals": "^2.1.0"
},
"scripts": {
@@ -46,5 +48,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"jest": "^30.1.1"
}
}

View File

@@ -1,129 +1,153 @@
import React, { useEffect, useState } from "react";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import CircularProgress from "@mui/material/CircularProgress";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
/**
* 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 {
Container,
Grid,
Typography,
Button,
Skeleton,
Box,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { FC } from "react";
import RoomList, { ConferenceRoom } from "../components/RoomList";
import BookingList, { Booking } from "../components/BookingList";
interface Room {
id: number;
name: string;
capacity: number;
}
interface Booking {
id: number;
room_id: number;
start_time: string;
end_time: string;
user: string;
}
const LandingPage: React.FC = () => {
const [rooms, setRooms] = useState<Room[]>([]);
const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
/**
* Renders the landing page with rooms, bookings, and navigation.
* Uses react-query for data fetching and skeleton loaders for loading states.
* @returns {JSX.Element} The landing page UI.
*/
const LandingPage: FC = () => {
const navigate = useNavigate();
const today = new Date().toISOString().split("T")[0];
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const roomsRes = await fetch("/api/rooms");
const bookingsRes = await fetch("/api/bookings/today");
if (!roomsRes.ok || !bookingsRes.ok)
throw new Error("Failed to fetch data");
setRooms(await roomsRes.json());
setBookings(await bookingsRes.json());
} catch (err: any) {
setError(err.message || "Unknown error");
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// Fetch rooms
const {
data: rooms = [],
isLoading: roomsLoading,
error: roomsError,
refetch: refetchRooms,
} = useQuery({
queryKey: ["rooms"],
queryFn: () =>
axios.get("/rooms/").then((res) => res.data as ConferenceRoom[]),
});
// Fetch today's bookings
const {
data: bookings = [],
isLoading: bookingsLoading,
error: bookingsError,
refetch: refetchBookings,
} = useQuery({
queryKey: ["bookings", today],
queryFn: () =>
axios
.get(`/bookings/room/?date=${today}`)
.then((res) => res.data as Booking[]),
});
// Display skeleton loaders during data fetching
if (roomsLoading || bookingsLoading) {
return (
<Container maxWidth="lg" sx={{ mt: 4 }}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<Box sx={{ p: 2 }}>
<Skeleton variant="text" width="50%" />
{[...Array(3)].map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
height={60}
sx={{ mb: 1 }}
/>
))}
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ p: 2 }}>
<Skeleton variant="text" width="50%" />
{[...Array(3)].map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
height={60}
sx={{ mb: 1 }}
/>
))}
</Box>
</Grid>
</Grid>
</Container>
);
}
// Display error message with retry option
if (roomsError || bookingsError) {
return (
<Container maxWidth="lg" sx={{ mt: 4, textAlign: "center" }}>
<Typography color="error">
{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>
</Container>
);
}
return (
<Box>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Conference Room Booking
</Typography>
<Button color="inherit" onClick={() => navigate("/bookings")}>
Bookings
</Button>
</Toolbar>
</AppBar>
<Container maxWidth="md" sx={{ mt: 4 }}>
<Typography variant="h4" gutterBottom>
Available Conference Rooms
</Typography>
{loading ? (
<CircularProgress />
) : error ? (
<Typography color="error">{error}</Typography>
) : (
<Grid container spacing={2}>
{rooms.map((room) => (
<Grid item xs={12} sm={6} md={4} key={room.id}>
<Card>
<CardContent>
<Typography variant="h6">{room.name}</Typography>
<Typography variant="body2">
Capacity: {room.capacity}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
<Box mt={4}>
<Typography variant="h5" gutterBottom>
Today's Bookings
</Typography>
{loading ? (
<CircularProgress />
) : error ? (
<Typography color="error">{error}</Typography>
) : bookings.length === 0 ? (
<Typography>No bookings for today.</Typography>
) : (
<Grid container spacing={2}>
{bookings.map((booking) => (
<Grid item xs={12} sm={6} md={4} key={booking.id}>
<Card>
<CardContent>
<Typography variant="subtitle1">
Room:{" "}
{rooms.find((r) => r.id === booking.room_id)?.name ||
"Unknown"}
</Typography>
<Typography variant="body2">
{booking.start_time} - {booking.end_time}
</Typography>
<Typography variant="body2">
Booked by: {booking.user}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
</Container>
</Box>
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Typography
variant="h4"
gutterBottom
align="center"
sx={{ fontSize: { xs: "1.5rem", md: "2.25rem" } }}
>
Conference Room Booking System
</Typography>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<RoomList rooms={rooms} />
</Grid>
<Grid item xs={12} md={6}>
<BookingList bookings={bookings} />
</Grid>
</Grid>
<Box sx={{ mt: 4, textAlign: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => navigate("/booking")}
aria-label="Navigate to booking page"
>
Book a Room
</Button>
</Box>
</Container>
);
};

File diff suppressed because it is too large Load Diff