diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5cc2502a..0ab60801 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,9 +6,9 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ -import { logger } from "./utils/logger"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { logger } from "./utils/logger"; import LandingPage from "./pages/LandingPage"; import BookingPage from "./pages/BookingPage"; import ConfirmationPage from "./pages/ConfirmationPage"; diff --git a/frontend/src/components/BookingConfirmation.tsx b/frontend/src/components/BookingConfirmation.tsx index ddd99f52..3a359e43 100644 --- a/frontend/src/components/BookingConfirmation.tsx +++ b/frontend/src/components/BookingConfirmation.tsx @@ -14,9 +14,9 @@ * @param {BookingConfirmationProps} props - Component props * @returns {JSX.Element} Confirmation UI */ + import React from "react"; import { Box, Button } from "@mui/material"; - import type { BookingConfirmationProps } from "../schemas"; /** diff --git a/frontend/src/components/BookingForm.tsx b/frontend/src/components/BookingForm.tsx index 6453455c..50b1408c 100644 --- a/frontend/src/components/BookingForm.tsx +++ b/frontend/src/components/BookingForm.tsx @@ -6,13 +6,15 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; - import { - formatLocalDateTimeInput, - roundToStrictlyFutureQuarter, -} from "../utils/date"; + getRoomBookings, + createBooking, + updateBooking, + deleteBooking, +} from "../apis/bookings"; import { getEditBookingRoomId, formatBookingError } from "../helpers/booking"; import { validateRoomId, @@ -23,11 +25,9 @@ import { validateRoomAvailability, } from "../helpers/validation"; import { - getRoomBookings, - createBooking, - updateBooking, - deleteBooking, -} from "../apis/bookings"; + formatLocalDateTimeInput, + roundToStrictlyFutureQuarter, +} from "../utils/date"; import { connectRoomsAvailabilityStream } from "../apis/sse"; import { getInvitees, addInvitee, removeInvitee } from "../apis/invitees"; import { getAvailableUsers } from "../apis/users"; diff --git a/frontend/src/components/BookingList.tsx b/frontend/src/components/BookingList.tsx index c82e1be0..14a6ca0c 100644 --- a/frontend/src/components/BookingList.tsx +++ b/frontend/src/components/BookingList.tsx @@ -9,7 +9,6 @@ import React from "react"; import { List, Typography, ListItemButton } from "@mui/material"; -// ...existing code... import { sortBookingsByStartTime, formatBookingTime, diff --git a/frontend/src/components/CalendarView.tsx b/frontend/src/components/CalendarView.tsx index deb00bcf..50587101 100644 --- a/frontend/src/components/CalendarView.tsx +++ b/frontend/src/components/CalendarView.tsx @@ -10,16 +10,17 @@ * CalendarView component. * Renders calendar with events, tooltips, and slot selection. */ + import React from "react"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; -import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; -import { EventInput } from "@fullcalendar/core"; +import timeGridPlugin from "@fullcalendar/timegrid"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import "../styles/CalendarView.css"; +import { EventInput } from "@fullcalendar/core"; import { getEventDisplayText, getRoomClass } from "../helpers/calendar"; +import "../styles/CalendarView.css"; /** * Props for CalendarView. diff --git a/frontend/src/components/RoomDetailsModal.tsx b/frontend/src/components/RoomDetailsModal.tsx index 2140e27d..5fc8feda 100644 --- a/frontend/src/components/RoomDetailsModal.tsx +++ b/frontend/src/components/RoomDetailsModal.tsx @@ -6,6 +6,7 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React from "react"; import { Dialog, @@ -16,7 +17,6 @@ import { Typography, Box, } from "@mui/material"; - import type { RoomDetailsModalProps } from "../schemas"; const RoomDetailsModal: React.FC = ({ diff --git a/frontend/src/components/RoomList.tsx b/frontend/src/components/RoomList.tsx index 258ef80a..e2fa8004 100644 --- a/frontend/src/components/RoomList.tsx +++ b/frontend/src/components/RoomList.tsx @@ -12,6 +12,7 @@ */ import React from "react"; +import { FC } from "react"; import { Card, CardContent, @@ -22,9 +23,8 @@ import { ListItemButton, } from "@mui/material"; import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; -import { FC } from "react"; -import type { RoomListProps } from "../schemas"; import { getRoomListItemStyles, getRoomSecondaryText } from "../helpers/room"; +import type { RoomListProps } from "../schemas"; /** * Renders a list of conference rooms with their details. diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index dcd6d63e..e36d4bda 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -5,16 +5,17 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React from "react"; import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; +import axios from "axios"; import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; import theme from "./theme"; import reportWebVitals from "./reportWebVitals"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import axios from "axios"; +import "./index.css"; // Global error handler to catch unhandled errors and prevent reloads window.onerror = function (message, source, lineno, colno, error) { diff --git a/frontend/src/pages/BookingPage.tsx b/frontend/src/pages/BookingPage.tsx index b7f560c6..4c5bbfff 100644 --- a/frontend/src/pages/BookingPage.tsx +++ b/frontend/src/pages/BookingPage.tsx @@ -6,12 +6,9 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React, { useMemo, useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; -import { getRooms } from "../apis/rooms"; -import { getUsers } from "../apis/users"; -import { getRoomBookings } from "../apis/bookings"; -import { connectRoomsAvailabilityStream } from "../apis/sse"; import { Button, MenuItem, @@ -20,8 +17,12 @@ import { FormControl, Box, } from "@mui/material"; -import CalendarView from "../components/CalendarView"; import { EventInput } from "@fullcalendar/core"; +import { getRooms } from "../apis/rooms"; +import { getUsers } from "../apis/users"; +import { getRoomBookings } from "../apis/bookings"; +import { connectRoomsAvailabilityStream } from "../apis/sse"; +import CalendarView from "../components/CalendarView"; import BookingForm from "../components/BookingForm"; import RoomDetailsModal from "../components/RoomDetailsModal"; import { logger } from "../utils/logger"; diff --git a/frontend/src/pages/ConfirmationPage.tsx b/frontend/src/pages/ConfirmationPage.tsx index 82a50182..b1cff5d8 100644 --- a/frontend/src/pages/ConfirmationPage.tsx +++ b/frontend/src/pages/ConfirmationPage.tsx @@ -6,14 +6,15 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React, { useState } from "react"; import { Box } from "@mui/material"; -import BookingConfirmation from "../components/BookingConfirmation"; -import BookingForm from "../components/BookingForm"; import { useNavigate, useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { getRooms } from "../apis/rooms"; import { connectRoomsAvailabilityStream } from "../apis/sse"; +import BookingConfirmation from "../components/BookingConfirmation"; +import BookingForm from "../components/BookingForm"; // This page expects booking data to be passed via location.state const ConfirmationPage: React.FC = () => { diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index e04c5dcd..c776f9ce 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -7,6 +7,7 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React, { FC, useEffect, useState } from "react"; import { Typography, @@ -16,6 +17,7 @@ import { Card, CardContent, } from "@mui/material"; +import EventNoteIcon from "@mui/icons-material/EventNote"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { getRooms } from "../apis/rooms"; @@ -24,9 +26,8 @@ import RoomList from "../components/RoomList"; import BookingList from "../components/BookingList"; import type { ConferenceRoom } from "../schemas"; import type { Booking } from "../schemas"; -import EventNoteIcon from "@mui/icons-material/EventNote"; -// import "./LandingPage.css"; import { logger } from "../utils/logger"; +// import "./LandingPage.css"; /** * LandingPage component diff --git a/frontend/src/styles/CalendarView.css b/frontend/src/styles/CalendarView.css new file mode 100644 index 00000000..c1c31e38 --- /dev/null +++ b/frontend/src/styles/CalendarView.css @@ -0,0 +1,226 @@ +/* + * CalendarView.css + * Styles for the calendar view component, including event colors and non-business hours. + * + * Author: Cliff Hill + * Last updated: 2025-09-05 + */ +/* Non-business hours styling for FullCalendar */ +.fc-timegrid-col-bg .fc-non-business, +.fc-non-business { + background: #888 !important; + opacity: 0.5 !important; +} + +/* Room color classes for calendar events */ +.room-color-0 { + background: #1976d2 !important; + color: #fff !important; +} +.room-color-1 { + background: #388e3c !important; + color: #fff !important; +} +.room-color-2 { + background: #fbc02d !important; + color: #fff !important; +} +.room-color-3 { + background: #d32f2f !important; + color: #fff !important; +} +.room-color-4 { + background: #7b1fa2 !important; + color: #fff !important; +} +.room-color-5 { + background: #0288d1 !important; + color: #fff !important; +} +.room-color-6 { + background: #c2185b !important; + color: #fff !important; +} +.room-color-7 { + background: #ffa000 !important; + color: #fff !important; +} +/* Truncate and ellipsis for event content and titles */ +.fc-event-title-ellipsis, +.fc-event .fc-event-main > div, +.fc-event-title, +.fc-event-main, +.fc-event .fc-event-main-frame, +.fc-event .fc-event-title, +.fc-event .fc-event-title-container, +.fc-event .fc-event-time, +.fc-event .fc-event-title, +.fc-event .fc-event-title-container, +.fc-event .fc-event-main { + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + max-width: 100% !important; + display: block !important; + width: 100%; + min-width: 0; +} + +/* Weekend all-day and time slots: unified non-business color, lighter border */ +.fc .fc-daygrid-day.fc-day-sat .fc-daygrid-day-frame, +.fc .fc-daygrid-day.fc-day-sun .fc-daygrid-day-frame, +.fc .fc-timegrid-col.fc-day-sat, +.fc .fc-timegrid-col.fc-day-sun { + background: #888 !important; /* unified non-business color */ + border-color: #bbb !important; +} + +.fc .fc-daygrid-day.fc-day-sat .fc-daygrid-day-number, +.fc .fc-daygrid-day.fc-day-sun .fc-daygrid-day-number { + color: #e0e0e0 !important; + font-weight: 500 !important; + opacity: 0.7 !important; +} + +/* Weekday non-business hours: same as weekend background */ +.fc .fc-non-business { + background: #888 !important; + opacity: 0.5 !important; +} + +/* Workday all-day and time slots: match all-day workday background */ +.fc .fc-daygrid-day .fc-daygrid-day-frame, +.fc .fc-timegrid-col { + background: #c0c0c0 !important; +} + +/* Lighter border for all-day row on weekends */ +.fc .fc-daygrid-day.fc-day-sat, +.fc .fc-daygrid-day.fc-day-sun { + border-color: #bbb !important; +} + +/* Out-of-month days: distinct but not too dark */ +.fc .fc-daygrid-day.fc-day-other .fc-daygrid-day-frame { + background: #aaa !important; + opacity: 1 !important; +} +/* Calendar cell backgrounds use CSS variables */ +:root { + --fc-page-bg-color: #a0a0a0; + --fc-neutral-bg-color: #e3f2fd; + --fc-today-bg-color: #e3f2fd; + --fc-event-bg-color: #1976d2; + --fc-border-color: #707070; +} + +.calendarContainer { + padding: 2rem; + background: #f8fafc !important; + border-radius: 16px !important; + box-shadow: 0 4px 24px rgba(30, 64, 175, 0.08) !important; + /* Responsive container, no fixed height */ +} + +.calendar-scroll-area { + flex: 1 1 auto; + overflow-y: auto; + min-height: 0; +} + +.fc { + font-family: "Inter", "Roboto", Arial, sans-serif !important; + box-shadow: none !important; +} + +.fc .fc-toolbar { + background: #1976d2 !important; + color: #fff !important; + border-radius: 12px 12px 0 0 !important; + padding: 0.5rem 1rem !important; +} + +.fc .fc-toolbar-title { + font-size: 1.5rem !important; + font-weight: 600 !important; +} + +.fc .fc-button { + background: #1976d2 !important; + color: #fff !important; + border: none !important; + border-radius: 6px !important; + padding: 0.3rem 0.8rem !important; + margin: 0 0.2rem !important; + font-weight: 500 !important; + transition: background 0.2s !important; +} + +.fc .fc-button:hover, +.fc .fc-button:focus { + background: #1565c0 !important; +} + +.fc .fc-daygrid-day { + box-shadow: 0 1px 4px rgba(30, 64, 175, 0.08) !important; + transition: box-shadow 0.2s !important; +} + +.fc .fc-daygrid-day:hover { + box-shadow: 0 4px 12px rgba(30, 64, 175, 0.16) !important; + background: #e3f2fd !important; +} + +.fc .fc-daygrid-day-number { + font-weight: 600 !important; + color: #1976d2 !important; +} + +.fc .fc-event { + background: #1976d2 !important; + color: #fff !important; + padding: 2px 6px !important; + font-size: 0.95rem !important; + box-shadow: 0 2px 8px rgba(30, 64, 175, 0.12) !important; +} + +.fc .fc-event:hover { + background: #1565c0 !important; +} + +.fc .fc-day-today { + background: #e3f2fd !important; + border-radius: 8px !important; +} + +.fc .fc-daygrid-day .fc-daygrid-day-frame { + background: #c0c0c0 !important; + box-shadow: 0 1px 4px rgba(30, 64, 175, 0.08) !important; + transition: box-shadow 0.2s !important; +} + +.fc .fc-daygrid-day.fc-day-today .fc-daygrid-day-frame { + background: #d0d0d0 !important; + box-shadow: 0 4px 12px rgba(30, 64, 175, 0.16) !important; +} + +.fc .fc-daygrid-day.fc-day-other .fc-daygrid-day-frame { + background: #aaa !important; + opacity: 1 !important; +} + +.fc .fc-daygrid-day.fc-day-other .fc-daygrid-day-number { + color: #fff !important; + opacity: 1 !important; + font-weight: 600 !important; +} + +.fc .fc-daygrid-day:hover .fc-daygrid-day-frame { + background: #e3f2fd !important; + box-shadow: 0 4px 12px rgba(30, 64, 175, 0.16) !important; +} + +.fc, +.fc-toolbar.fc-header-toolbar { + margin-bottom: 0px !important; +}