mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-09 17:45:35 -04:00
Working on the landing page.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
1083
frontend/yarn.lock
1083
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user