booking-flow-finalized-design kindaaaa #7
2
my-app/.gitignore
vendored
2
my-app/.gitignore
vendored
@@ -29,3 +29,5 @@ storybook-static
|
||||
# Font files
|
||||
public/caecilia/
|
||||
public/the-sans/
|
||||
|
||||
deploy.sh
|
||||
@@ -14,6 +14,7 @@ import { TestSession } from './pages/TestSession';
|
||||
import Home from './pages/Home';
|
||||
import CoursePage from './pages/CoursePage';
|
||||
import Profile from './pages/Profile';
|
||||
import RoomSchedules from './pages/RoomSchedules';
|
||||
|
||||
const AppRoutes = () => {
|
||||
const location = useLocation();
|
||||
@@ -129,6 +130,7 @@ const AppRoutes = () => {
|
||||
<Route path="home" element={<Home />} />
|
||||
<Route path="course/:courseId" element={<CoursePage />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
<Route path="room-schedules" element={<RoomSchedules />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</>
|
||||
|
||||
236
my-app/src/pages/RoomSchedules.jsx
Normal file
236
my-app/src/pages/RoomSchedules.jsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { useState } from 'react';
|
||||
import styles from './RoomSchedules.module.css';
|
||||
|
||||
const RoomSchedules = () => {
|
||||
const [selectedBooking, setSelectedBooking] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const timeSlots = [
|
||||
'08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00',
|
||||
'15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00',
|
||||
'22:00', '23:00', '00:00'
|
||||
];
|
||||
|
||||
const rooms = [
|
||||
{
|
||||
name: 'Lilla hörsalen',
|
||||
bookings: [
|
||||
{
|
||||
id: 1,
|
||||
course: 'MDI S2 grp 7',
|
||||
time: '08:00 - 08:30',
|
||||
instructor: 'Anna Andersson',
|
||||
participants: 12,
|
||||
description: 'Medicinsk datavetenskap seminarium'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
course: 'MDI S2 grp 7',
|
||||
time: '08:30 - 09:00',
|
||||
instructor: 'Anna Andersson',
|
||||
participants: 12,
|
||||
description: 'Medicinsk datavetenskap seminarium'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
course: 'INTROPROG F12',
|
||||
time: '09:00 - 09:30',
|
||||
instructor: 'Erik Larsson',
|
||||
participants: 25,
|
||||
description: 'Introduktion till programmering - föreläsning'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Aula NOD',
|
||||
bookings: [
|
||||
{
|
||||
id: 4,
|
||||
course: 'MDI S2 grp 7',
|
||||
time: '08:00 - 08:30',
|
||||
instructor: 'Anna Andersson',
|
||||
participants: 12,
|
||||
description: 'Medicinsk datavetenskap seminarium'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
course: 'MDI S2 grp 7',
|
||||
time: '08:30 - 09:00',
|
||||
instructor: 'Anna Andersson',
|
||||
participants: 12,
|
||||
description: 'Medicinsk datavetenskap seminarium'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
course: 'INTROPROG F12',
|
||||
time: '09:00 - 09:30',
|
||||
instructor: 'Erik Larsson',
|
||||
participants: 25,
|
||||
description: 'Introduktion till programmering - föreläsning'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
course: 'DB L6 grp 2',
|
||||
time: '10:00 - 12:45',
|
||||
instructor: 'Sofia Karlsson',
|
||||
participants: 18,
|
||||
description: 'Databaser - laboration 6'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
course: 'INTROPROG F12',
|
||||
time: '13:00 - 14:00',
|
||||
instructor: 'Erik Larsson',
|
||||
participants: 25,
|
||||
description: 'Introduktion till programmering - föreläsning'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'G5:7',
|
||||
bookings: [
|
||||
{
|
||||
id: 9,
|
||||
course: 'Team standup',
|
||||
time: '10:00 - 12:00',
|
||||
instructor: 'Magnus Nilsson',
|
||||
participants: 5,
|
||||
description: 'Daglig standup för utvecklingsteam'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'G5:12',
|
||||
bookings: [
|
||||
{
|
||||
id: 10,
|
||||
course: 'Project planning',
|
||||
time: '16:00 - 20:00',
|
||||
instructor: 'Emma Johansson',
|
||||
participants: 8,
|
||||
description: 'Projektplanering för kommande sprint'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const getBookingForTimeSlot = (room, timeSlot) => {
|
||||
return room.bookings.find(booking => {
|
||||
const [startTime] = booking.time.split(' - ');
|
||||
return startTime === timeSlot;
|
||||
});
|
||||
};
|
||||
|
||||
const getBookingSpan = (booking) => {
|
||||
const [startTime, endTime] = booking.time.split(' - ');
|
||||
const startHour = parseInt(startTime.split(':')[0]);
|
||||
const endHour = parseInt(endTime.split(':')[0]);
|
||||
const startMinutes = parseInt(startTime.split(':')[1]);
|
||||
const endMinutes = parseInt(endTime.split(':')[1]);
|
||||
|
||||
const startTotalMinutes = startHour * 60 + startMinutes;
|
||||
const endTotalMinutes = endHour * 60 + endMinutes;
|
||||
const durationMinutes = endTotalMinutes - startTotalMinutes;
|
||||
|
||||
return Math.max(1, Math.round(durationMinutes / 60));
|
||||
};
|
||||
|
||||
const handleBookingClick = (booking, roomName) => {
|
||||
setSelectedBooking({ ...booking, room: roomName });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
setSelectedBooking(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.RoomSchedulesContainer}>
|
||||
<h1>Room Schedules</h1>
|
||||
|
||||
<div className={styles.ScheduleWrapper}>
|
||||
<div className={styles.TimeColumn}>
|
||||
<div className={styles.TimeHeader}></div>
|
||||
{timeSlots.map((time, index) => (
|
||||
<div key={index} className={styles.TimeSlot}>
|
||||
{time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.RoomsContainer}>
|
||||
{rooms.map((room, roomIndex) => (
|
||||
<div key={roomIndex} className={styles.RoomColumn}>
|
||||
<div className={styles.RoomHeader}>
|
||||
{room.name}
|
||||
</div>
|
||||
|
||||
{timeSlots.map((timeSlot, timeIndex) => {
|
||||
const booking = getBookingForTimeSlot(room, timeSlot);
|
||||
const isOccupied = booking !== undefined;
|
||||
const span = booking ? getBookingSpan(booking) : 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={timeIndex}
|
||||
className={`${styles.ScheduleSlot} ${isOccupied ? styles.Occupied : ''}`}
|
||||
style={{
|
||||
gridRowEnd: isOccupied ? `span ${span}` : 'span 1'
|
||||
}}
|
||||
>
|
||||
{isOccupied && (
|
||||
<div
|
||||
className={styles.BookingInfo}
|
||||
onClick={() => handleBookingClick(booking, room.name)}
|
||||
>
|
||||
<div className={styles.CourseName}>{booking.course}</div>
|
||||
<div className={styles.BookingTime}>{booking.time}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showModal && selectedBooking && (
|
||||
<div className={styles.ModalOverlay} onClick={closeModal}>
|
||||
<div className={styles.Modal} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.ModalHeader}>
|
||||
<h2>{selectedBooking.course}</h2>
|
||||
<button className={styles.CloseButton} onClick={closeModal}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.ModalContent}>
|
||||
<div className={styles.DetailRow}>
|
||||
<span className={styles.Label}>Room:</span>
|
||||
<span>{selectedBooking.room}</span>
|
||||
</div>
|
||||
<div className={styles.DetailRow}>
|
||||
<span className={styles.Label}>Time:</span>
|
||||
<span>{selectedBooking.time}</span>
|
||||
</div>
|
||||
<div className={styles.DetailRow}>
|
||||
<span className={styles.Label}>Instructor:</span>
|
||||
<span>{selectedBooking.instructor}</span>
|
||||
</div>
|
||||
<div className={styles.DetailRow}>
|
||||
<span className={styles.Label}>Participants:</span>
|
||||
<span>{selectedBooking.participants}</span>
|
||||
</div>
|
||||
<div className={styles.DetailRow}>
|
||||
<span className={styles.Label}>Description:</span>
|
||||
<span>{selectedBooking.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomSchedules;
|
||||
249
my-app/src/pages/RoomSchedules.module.css
Normal file
249
my-app/src/pages/RoomSchedules.module.css
Normal file
@@ -0,0 +1,249 @@
|
||||
.RoomSchedulesContainer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.RoomSchedulesContainer h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ScheduleWrapper {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.TimeColumn {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.TimeHeader {
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.TimeSlot {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.RoomsContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.RoomColumn {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
display: grid;
|
||||
grid-template-rows: 60px repeat(17, 60px);
|
||||
}
|
||||
|
||||
.RoomColumn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.RoomHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f0f0f0;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ScheduleSlot {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f9f9f9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ScheduleSlot.Occupied {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196f3;
|
||||
margin: 1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.BookingInfo {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.BookingInfo:hover {
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.CourseName {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #1976d2;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.BookingTime {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.ModalOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
background: blue;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.ModalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.ModalHeader h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.CloseButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.CloseButton:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.ModalContent {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.DetailRow {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.DetailRow:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.Label {
|
||||
font-weight: 600;
|
||||
min-width: 100px;
|
||||
color: #666;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* Mobile scrolling */
|
||||
@media (max-width: 768px) {
|
||||
.ScheduleWrapper {
|
||||
overflow-x: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.RoomColumn {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.TimeColumn {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.RoomHeader {
|
||||
font-size: 0.9rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.CourseName {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.BookingTime {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.ModalHeader {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ModalContent {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Label {
|
||||
min-width: 80px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user