From efd62e073362a29421da14dca9a8d19f19d1f1e4 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:53:32 +0200 Subject: [PATCH 01/20] first --- my-app/.gitignore | 2 + my-app/src/AppRoutes.jsx | 2 + my-app/src/pages/RoomSchedules.jsx | 236 ++++++++++++++++++++ my-app/src/pages/RoomSchedules.module.css | 249 ++++++++++++++++++++++ 4 files changed, 489 insertions(+) create mode 100644 my-app/src/pages/RoomSchedules.jsx create mode 100644 my-app/src/pages/RoomSchedules.module.css diff --git a/my-app/.gitignore b/my-app/.gitignore index 0a776fb..38b32b6 100644 --- a/my-app/.gitignore +++ b/my-app/.gitignore @@ -29,3 +29,5 @@ storybook-static # Font files public/caecilia/ public/the-sans/ + +deploy.sh \ No newline at end of file diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 0ce08f6..e86a380 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -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 = () => { } /> } /> } /> + } /> diff --git a/my-app/src/pages/RoomSchedules.jsx b/my-app/src/pages/RoomSchedules.jsx new file mode 100644 index 0000000..6896629 --- /dev/null +++ b/my-app/src/pages/RoomSchedules.jsx @@ -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 ( +
+

Room Schedules

+ +
+
+
+ {timeSlots.map((time, index) => ( +
+ {time} +
+ ))} +
+ +
+ {rooms.map((room, roomIndex) => ( +
+
+ {room.name} +
+ + {timeSlots.map((timeSlot, timeIndex) => { + const booking = getBookingForTimeSlot(room, timeSlot); + const isOccupied = booking !== undefined; + const span = booking ? getBookingSpan(booking) : 1; + + return ( +
+ {isOccupied && ( +
handleBookingClick(booking, room.name)} + > +
{booking.course}
+
{booking.time}
+
+ )} +
+ ); + })} +
+ ))} +
+
+ + {showModal && selectedBooking && ( +
+
e.stopPropagation()}> +
+

{selectedBooking.course}

+ +
+
+
+ Room: + {selectedBooking.room} +
+
+ Time: + {selectedBooking.time} +
+
+ Instructor: + {selectedBooking.instructor} +
+
+ Participants: + {selectedBooking.participants} +
+
+ Description: + {selectedBooking.description} +
+
+
+
+ )} +
+ ); +}; + +export default RoomSchedules; \ No newline at end of file diff --git a/my-app/src/pages/RoomSchedules.module.css b/my-app/src/pages/RoomSchedules.module.css new file mode 100644 index 0000000..578e0d6 --- /dev/null +++ b/my-app/src/pages/RoomSchedules.module.css @@ -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; + } +} \ No newline at end of file -- 2.39.5 From 629700e6dc524349eab99093765399c31bf84d79 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:44:06 +0200 Subject: [PATCH 02/20] page container component and timecard fixes --- .../src/components/layout/PageContainer.jsx | 12 + .../layout/PageContainer.module.css | 6 + my-app/src/components/layout/PageHeader.jsx | 13 + .../components/layout/PageHeader.module.css | 19 ++ .../src/components/ui/TimeCardContainer.jsx | 316 +++++++++--------- .../ui/TimeCardContainer.module.css | 58 +++- my-app/src/pages/NewBooking.jsx | 8 +- my-app/src/pages/RoomBooking.jsx | 6 +- my-app/src/pages/RoomSchedules.jsx | 11 +- my-app/src/pages/RoomSchedules.module.css | 7 - 10 files changed, 285 insertions(+), 171 deletions(-) create mode 100644 my-app/src/components/layout/PageContainer.jsx create mode 100644 my-app/src/components/layout/PageContainer.module.css create mode 100644 my-app/src/components/layout/PageHeader.jsx create mode 100644 my-app/src/components/layout/PageHeader.module.css diff --git a/my-app/src/components/layout/PageContainer.jsx b/my-app/src/components/layout/PageContainer.jsx new file mode 100644 index 0000000..599120b --- /dev/null +++ b/my-app/src/components/layout/PageContainer.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './PageContainer.module.css'; + +const PageContainer = ({ children, className = '' }) => { + return ( +
+ {children} +
+ ); +}; + +export default PageContainer; \ No newline at end of file diff --git a/my-app/src/components/layout/PageContainer.module.css b/my-app/src/components/layout/PageContainer.module.css new file mode 100644 index 0000000..d81e635 --- /dev/null +++ b/my-app/src/components/layout/PageContainer.module.css @@ -0,0 +1,6 @@ +.pageContainer { + max-width: 1200px; + margin: 0 auto; + padding: var(--spacing-2xl); + min-height: 100vh; +} \ No newline at end of file diff --git a/my-app/src/components/layout/PageHeader.jsx b/my-app/src/components/layout/PageHeader.jsx new file mode 100644 index 0000000..43983aa --- /dev/null +++ b/my-app/src/components/layout/PageHeader.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './PageHeader.module.css'; + +const PageHeader = ({ title, subtitle }) => { + return ( +
+

{title}

+ {subtitle &&
{subtitle}
} +
+ ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/my-app/src/components/layout/PageHeader.module.css b/my-app/src/components/layout/PageHeader.module.css new file mode 100644 index 0000000..6de91a9 --- /dev/null +++ b/my-app/src/components/layout/PageHeader.module.css @@ -0,0 +1,19 @@ +.header { + margin-bottom: var(--spacing-2xl); + padding-bottom: var(--spacing-xl); + border-bottom: 1px solid var(--border-light); +} + +.pageHeading { + color: var(--text-primary); + margin: 0 0 var(--spacing-md) 0; + font-size: 2.5rem; + font-weight: var(--font-weight-bold); + line-height: 1.2; +} + +.subtitle { + color: var(--text-secondary); + font-size: 1.1rem; + font-weight: var(--font-weight-medium); +} \ No newline at end of file diff --git a/my-app/src/components/ui/TimeCardContainer.jsx b/my-app/src/components/ui/TimeCardContainer.jsx index e08665a..6069ecf 100644 --- a/my-app/src/components/ui/TimeCardContainer.jsx +++ b/my-app/src/components/ui/TimeCardContainer.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import TimeCard from './TimeCard'; import { InlineBookingForm } from '../booking/InlineBookingForm'; @@ -11,12 +11,30 @@ import modalStyles from '../booking/BookingModal.module.css'; import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; -const SLOT_GROUPING_SIZE = 8; - export function TimeCardContainer({ addBooking }) { const navigate = useNavigate(); const booking = useBookingContext(); const { settings } = useSettingsContext(); + const [slotGroupingSize, setSlotGroupingSize] = useState(8); + + const LARGE_BREAKPOINT = 1400; + + useEffect(() => { + const updateGroupingSize = () => { + const width = window.innerWidth; + if (width >= LARGE_BREAKPOINT + 1) { + setSlotGroupingSize(8); // 3 columns (24÷8=3) + } else if (width >= 769) { + setSlotGroupingSize(12); // 2 columns (24÷12=2) + } else { + setSlotGroupingSize(24); // 1 column (24÷24=1) + } + }; + + updateGroupingSize(); + window.addEventListener('resize', updateGroupingSize); + return () => window.removeEventListener('resize', updateGroupingSize); + }, []); // Check booking form type const useInlineForm = settings.bookingFormType === 'inline'; @@ -59,161 +77,155 @@ export function TimeCardContainer({ addBooking }) { function slotIndiciesToColumns(originalArray) { - let newArray = []; - let currentColumn = -1; - - originalArray.map(index => { - if (index % SLOT_GROUPING_SIZE == 0) { - newArray.push(new Array(0)); - currentColumn++; + const width = window.innerWidth; + + if (width >= 769 && width <= LARGE_BREAKPOINT) { + // For medium screens: group in pairs first, then distribute pairs across 2 columns + const pairs = []; + for (let i = 0; i < originalArray.length; i += 2) { + pairs.push([originalArray[i], originalArray[i + 1]]); } - newArray[currentColumn].push(originalArray[index]); - }); - return newArray; + + // Distribute pairs across 2 columns (6 pairs per column) + const column1 = pairs.slice(0, 6).flat(); + const column2 = pairs.slice(6, 12).flat(); + return [column1, column2]; + } else { + // For other screen sizes: use original grouping logic + let newArray = []; + let currentColumn = -1; + + originalArray.map(index => { + if (index % slotGroupingSize == 0) { + newArray.push(new Array(0)); + currentColumn++; + } + newArray[currentColumn].push(originalArray[index]); + }); + return newArray; + } } + const renderColumn = (column, columnIndex) => { + const width = window.innerWidth; + + if (width >= 769 && width <= LARGE_BREAKPOINT) { + // For medium screens: render pairs in rows + const pairs = []; + for (let i = 0; i < column.length; i += 2) { + pairs.push([column[i], column[i + 1]]); + } + + return ( +
+ {pairs.map((pair, pairIndex) => ( +
+ {pair.map(slotIndex => renderTimeCard(slotIndex))} +
+ ))} +
+ ); + } else if (width < 769) { + // For mobile: render pairs with spacing between every 4 pairs + const pairs = []; + for (let i = 0; i < column.length; i += 2) { + pairs.push([column[i], column[i + 1]]); + } + + return ( +
+ {pairs.map((pair, pairIndex) => ( +
+
+ {pair.map(slotIndex => renderTimeCard(slotIndex))} +
+ {/* Add spacing after every 4th pair */} + {(pairIndex + 1) % 4 === 0 && pairIndex < pairs.length - 1 && ( +
+ )} +
+ ))} +
+ ); + } else { + // For large screens: render normally + return ( +
+ {column.map(slotIndex => renderTimeCard(slotIndex))} +
+ ); + } + }; + + const renderTimeCard = (slotIndex) => { + let maxConsecutive = 0; + let roomId = ""; + + if (booking.currentRoom) { + const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex); + if (consecutive > maxConsecutive) { + maxConsecutive = consecutive; + } + } else { + booking.timeSlotsByRoom.forEach(room => { + const consecutive = countConsecutiveFromSlot(room.times, slotIndex); + if (consecutive > maxConsecutive) { + maxConsecutive = consecutive; + roomId = room.roomId; + } + }); + } + + if (booking.selectedBookingLength !== 0 && booking.selectedBookingLength <= maxConsecutive) { + maxConsecutive = booking.selectedBookingLength; + } + + /* Set time card state here: */ + let timeCardState = "unavailableSlot"; + + // If a booking length is pre-selected, only show slots that can accommodate the exact length + if (booking.selectedBookingLength !== 0) { + // Check if this slot can accommodate the selected booking length + const actualConsecutive = booking.currentRoom ? + countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) : + Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex))); + + if (actualConsecutive >= booking.selectedBookingLength) { + timeCardState = "availableSlot"; + } + } else { + // No pre-selected length, show if any time is available + if (maxConsecutive > 0) { + timeCardState = "availableSlot"; + } + } + + return ( + { + if (booking.selectedStartIndex === slotIndex) { + // If clicking on already selected card, close the form + booking.resetTimeSelections(); + } else { + // Otherwise, select this card + booking.handleTimeCardClick(slotIndex, maxConsecutive, roomId); + } + }} + selected={slotIndex === booking.selectedStartIndex} + state={timeCardState} + handleTimeCardHover={() => {}} + handleTimeCardExit={booking.handleTimeCardExit} + /> + ); + }; + return (
- {slotIndiciesToColumns(slotIndices).map((column, index) => { - - return ( -
- {column.map(slotIndex => { - let maxConsecutive = 0; - let roomId = ""; - - if (booking.currentRoom) { - const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex); - if (consecutive > maxConsecutive) { - maxConsecutive = consecutive; - } - } else { - booking.timeSlotsByRoom.forEach(room => { - const consecutive = countConsecutiveFromSlot(room.times, slotIndex); - if (consecutive > maxConsecutive) { - maxConsecutive = consecutive; - roomId = room.roomId; - } - }); - } - - if (booking.selectedBookingLength !== 0 && booking.selectedBookingLength <= maxConsecutive) { - maxConsecutive = booking.selectedBookingLength; - } - - /* Set time card state here: */ - let timeCardState = "unavailableSlot"; - - // If a booking length is pre-selected, only show slots that can accommodate the exact length - if (booking.selectedBookingLength !== 0) { - // Check if this slot can accommodate the selected booking length - const actualConsecutive = booking.currentRoom ? - countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) : - Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex))); - - if (actualConsecutive >= booking.selectedBookingLength) { - timeCardState = "availableSlot"; - } - } else { - // No pre-selected length, show if any time is available - if (maxConsecutive > 0) { - timeCardState = "availableSlot"; - } - } - - const elements = []; - - elements.push( - { - if (booking.selectedStartIndex === slotIndex) { - // If clicking on already selected card, close the form - booking.resetTimeSelections(); - } else { - // Otherwise, select this card - booking.handleTimeCardClick(slotIndex, maxConsecutive, roomId); - } - }} - selected={slotIndex === booking.selectedStartIndex} - state={timeCardState} - handleTimeCardHover={() => {}} - handleTimeCardExit={booking.handleTimeCardExit} - /> - ); - - // Add inline booking form after the pair that contains the selected time card - // Cards are laid out in pairs: (0,1), (2,3), (4,5), etc. - if ((useInlineForm || useInlineModal || useInlineModalExtended || useInlineModalExtendedNoLabels) && booking.selectedStartIndex !== null) { - const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2; - const selectedPairEnd = selectedPairStart + 1; - - // Show form after the second card of the pair that contains the selected card - if (slotIndex === selectedPairEnd && (booking.selectedStartIndex === selectedPairStart || booking.selectedStartIndex === selectedPairEnd)) { - const isLeftCard = booking.selectedStartIndex === selectedPairStart; - - if (useInlineForm) { - elements.push( - booking.resetTimeSelections()} - arrowPointsLeft={isLeftCard} - /> - ); - } else if (useInlineModal) { - elements.push( - booking.resetTimeSelections()} - onNavigateToDetails={handleNavigateToDetails} - arrowPointsLeft={isLeftCard} - /> - ); - } else if (useInlineModalExtended) { - elements.push( - booking.resetTimeSelections()} - onNavigateToDetails={handleNavigateToDetails} - addBooking={addBooking} - arrowPointsLeft={isLeftCard} - /> - ); - } else if (useInlineModalExtendedNoLabels) { - elements.push( - booking.resetTimeSelections()} - onNavigateToDetails={handleNavigateToDetails} - arrowPointsLeft={isLeftCard} - /> - ); - } - } - } - - return elements; - }).flat()} -
- ) - })} + {slotIndiciesToColumns(slotIndices).map((column, index) => renderColumn(column, index))}
{/* Show modal when a time slot is selected and using modal form type */} diff --git a/my-app/src/components/ui/TimeCardContainer.module.css b/my-app/src/components/ui/TimeCardContainer.module.css index aa8fd9f..2a7519c 100644 --- a/my-app/src/components/ui/TimeCardContainer.module.css +++ b/my-app/src/components/ui/TimeCardContainer.module.css @@ -1,7 +1,7 @@ .columnContainer { display: flex; flex-direction: row; - gap: 3rem; + gap: 2rem; height: fit-content } @@ -35,10 +35,64 @@ display: none; /* Chrome, Safari, Opera */ } -@media (max-width: 1200px) { +.pairRow { + display: flex; + flex-direction: row; + gap: 0.5rem; + width: 100%; + justify-content: center; +} + +/* Medium screens - 2 columns with 6 rows each */ +@media (max-width: 1400px) and (min-width: 769px) { + .columnContainer { + display: flex; + flex-direction: row; + gap: 3rem; + justify-content: center; + } + + .column { + min-width: 300px; + width: fit-content; + display: flex; + flex-direction: column; + gap: 0.5rem; + height: fit-content; + align-items: flex-start; + justify-content: flex-start; + } + + .pairRow { + display: flex; + flex-direction: row; + gap: 0.5rem; + width: 100%; + } + + .pairRow > * { + flex: 0 0 135px; + width: 135px; + } +} + +/* Small screens - single column */ +@media (max-width: 768px) { .columnContainer { display: flex; flex-direction: column; gap: 3rem; } + + .column { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + align-items: center; + } + + .groupSpacer { + height: 1.5rem; + } } \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 54ac102..5afc620 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -7,6 +7,8 @@ import { BookingLengthField } from '../components/forms/BookingLengthField'; import { useBookingState } from '../hooks/useBookingState'; import { BookingProvider } from '../context/BookingContext'; import { useSettingsContext } from '../context/SettingsContext'; +import PageHeader from '../components/layout/PageHeader'; +import PageContainer from '../components/layout/PageContainer'; export function NewBooking({ addBooking }) { const { getEffectiveToday, settings } = useSettingsContext(); @@ -58,8 +60,8 @@ export function NewBooking({ addBooking }) { return ( -
-

Boka litet grupprum

+ +
@@ -123,7 +125,7 @@ export function NewBooking({ addBooking }) {
-
+ ); } \ No newline at end of file diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index a6599d5..9a6cc57 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -5,6 +5,7 @@ import BookingsList from '../components/booking/BookingsList'; import Card from '../components/ui/Card'; import { useSettingsContext } from '../context/SettingsContext'; import { USER } from '../constants/bookingConstants'; +import PageHeader from '../components/layout/PageHeader'; export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) { const { settings } = useSettingsContext(); @@ -32,10 +33,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o )} -
-

Lokalbokning

-
Reservera lokaler för möten och studier
-
+

Ny bokning

diff --git a/my-app/src/pages/RoomSchedules.jsx b/my-app/src/pages/RoomSchedules.jsx index 6896629..4ade932 100644 --- a/my-app/src/pages/RoomSchedules.jsx +++ b/my-app/src/pages/RoomSchedules.jsx @@ -1,5 +1,7 @@ import React, { useState } from 'react'; import styles from './RoomSchedules.module.css'; +import PageHeader from '../components/layout/PageHeader'; +import PageContainer from '../components/layout/PageContainer'; const RoomSchedules = () => { const [selectedBooking, setSelectedBooking] = useState(null); @@ -145,8 +147,11 @@ const RoomSchedules = () => { }; return ( -
-

Room Schedules

+ +
@@ -229,7 +234,7 @@ const RoomSchedules = () => {
)} -
+ ); }; diff --git a/my-app/src/pages/RoomSchedules.module.css b/my-app/src/pages/RoomSchedules.module.css index 578e0d6..f9e28b5 100644 --- a/my-app/src/pages/RoomSchedules.module.css +++ b/my-app/src/pages/RoomSchedules.module.css @@ -1,10 +1,3 @@ -.RoomSchedulesContainer { - padding: 1rem; -} - -.RoomSchedulesContainer h1 { - margin-bottom: 1rem; -} .ScheduleWrapper { display: flex; -- 2.39.5 From 1ab912cbf2a8ed8b81e370af97ce1944569e51e8 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:18:15 +0200 Subject: [PATCH 03/20] timecard container bug fixes --- .../booking/InlineModalBookingForm.module.css | 2 +- my-app/src/components/layout/PageHeader.jsx | 13 +- .../components/layout/PageHeader.module.css | 19 +++ .../src/components/ui/TimeCardContainer.jsx | 146 ++++++++++++++++-- .../ui/TimeCardContainer.module.css | 2 +- my-app/src/pages/NewBooking.jsx | 3 +- my-app/src/pages/NewBooking.module.css | 9 +- .../src/react-aria-starter/src/DatePicker.css | 2 +- 8 files changed, 171 insertions(+), 25 deletions(-) diff --git a/my-app/src/components/booking/InlineModalBookingForm.module.css b/my-app/src/components/booking/InlineModalBookingForm.module.css index 380b380..0a05075 100644 --- a/my-app/src/components/booking/InlineModalBookingForm.module.css +++ b/my-app/src/components/booking/InlineModalBookingForm.module.css @@ -8,11 +8,11 @@ animation: slideDown 0.2s ease-out; width: 100%; flex-basis: 100%; - max-width: none; position: relative; z-index: 1; } + /* Arrow pointing to left card */ .arrowLeft::before { content: ''; diff --git a/my-app/src/components/layout/PageHeader.jsx b/my-app/src/components/layout/PageHeader.jsx index 43983aa..233e54c 100644 --- a/my-app/src/components/layout/PageHeader.jsx +++ b/my-app/src/components/layout/PageHeader.jsx @@ -1,11 +1,18 @@ import React from 'react'; import styles from './PageHeader.module.css'; -const PageHeader = ({ title, subtitle }) => { +const PageHeader = ({ title, subtitle, imageUrl }) => { return (
-

{title}

- {subtitle &&
{subtitle}
} + {imageUrl && ( +
+ {title} +
+ )} +
+

{title}

+ {subtitle &&
{subtitle}
} +
); }; diff --git a/my-app/src/components/layout/PageHeader.module.css b/my-app/src/components/layout/PageHeader.module.css index 6de91a9..cc37aee 100644 --- a/my-app/src/components/layout/PageHeader.module.css +++ b/my-app/src/components/layout/PageHeader.module.css @@ -2,6 +2,25 @@ margin-bottom: var(--spacing-2xl); padding-bottom: var(--spacing-xl); border-bottom: 1px solid var(--border-light); + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +} + +.imageContainer { + flex-shrink: 0; +} + +.image { + width: 100%; + aspect-ratio: 7 / 3; + object-fit: cover; +} + +.textContent { + flex: 1; + padding: 1rem; } .pageHeading { diff --git a/my-app/src/components/ui/TimeCardContainer.jsx b/my-app/src/components/ui/TimeCardContainer.jsx index 6069ecf..301c922 100644 --- a/my-app/src/components/ui/TimeCardContainer.jsx +++ b/my-app/src/components/ui/TimeCardContainer.jsx @@ -118,11 +118,30 @@ export function TimeCardContainer({ addBooking }) { return (
- {pairs.map((pair, pairIndex) => ( -
- {pair.map(slotIndex => renderTimeCard(slotIndex))} -
- ))} + {pairs.map((pair, pairIndex) => { + const elements = []; + + // Render the pair + elements.push( +
+ {pair.map(slotIndex => renderTimeCard(slotIndex))} +
+ ); + + // Check if we need to show inline form after this pair + const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2; + const pairStart = pair[0]; + const shouldShowForm = (useInlineForm || useInlineModal || useInlineModalExtended || useInlineModalExtendedNoLabels) && + booking.selectedStartIndex !== null && + pairStart === selectedPairStart; + + if (shouldShowForm) { + const isLeftCard = booking.selectedStartIndex === selectedPairStart; + elements.push(renderInlineForm(pairIndex, isLeftCard)); + } + + return elements; + }).flat()}
); } else if (width < 769) { @@ -134,29 +153,122 @@ export function TimeCardContainer({ addBooking }) { return (
- {pairs.map((pair, pairIndex) => ( -
-
- {pair.map(slotIndex => renderTimeCard(slotIndex))} + {pairs.map((pair, pairIndex) => { + const elements = []; + + // Render the pair + elements.push( +
+
+ {pair.map(slotIndex => renderTimeCard(slotIndex))} +
+ {/* Add spacing after every 4th pair */} + {(pairIndex + 1) % 4 === 0 && pairIndex < pairs.length - 1 && ( +
+ )}
- {/* Add spacing after every 4th pair */} - {(pairIndex + 1) % 4 === 0 && pairIndex < pairs.length - 1 && ( -
- )} -
- ))} + ); + + // Check if we need to show inline form after this pair + const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2; + const pairStart = pair[0]; + const shouldShowForm = (useInlineForm || useInlineModal || useInlineModalExtended || useInlineModalExtendedNoLabels) && + booking.selectedStartIndex !== null && + pairStart === selectedPairStart; + + if (shouldShowForm) { + const isLeftCard = booking.selectedStartIndex === selectedPairStart; + elements.push(renderInlineForm(pairIndex, isLeftCard)); + } + + return elements; + }).flat()}
); } else { - // For large screens: render normally + // For large screens: render normally with original logic return (
- {column.map(slotIndex => renderTimeCard(slotIndex))} + {column.map(slotIndex => { + const elements = []; + + elements.push(renderTimeCard(slotIndex)); + + // Add inline booking form after the pair that contains the selected time card + // Cards are laid out in pairs: (0,1), (2,3), (4,5), etc. + if ((useInlineForm || useInlineModal || useInlineModalExtended || useInlineModalExtendedNoLabels) && booking.selectedStartIndex !== null) { + const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2; + const selectedPairEnd = selectedPairStart + 1; + + // Show form after the second card of the pair that contains the selected card + if (slotIndex === selectedPairEnd && (booking.selectedStartIndex === selectedPairStart || booking.selectedStartIndex === selectedPairEnd)) { + const isLeftCard = booking.selectedStartIndex === selectedPairStart; + elements.push(renderInlineForm(slotIndex, isLeftCard)); + } + } + + return elements; + }).flat()}
); } }; + const renderInlineForm = (key, isLeftCard) => { + if (useInlineForm) { + return ( + booking.resetTimeSelections()} + arrowPointsLeft={isLeftCard} + /> + ); + } else if (useInlineModal) { + return ( + booking.resetTimeSelections()} + onNavigateToDetails={handleNavigateToDetails} + arrowPointsLeft={isLeftCard} + /> + ); + } else if (useInlineModalExtended) { + return ( + booking.resetTimeSelections()} + onNavigateToDetails={handleNavigateToDetails} + addBooking={addBooking} + arrowPointsLeft={isLeftCard} + /> + ); + } else if (useInlineModalExtendedNoLabels) { + return ( + booking.resetTimeSelections()} + onNavigateToDetails={handleNavigateToDetails} + arrowPointsLeft={isLeftCard} + /> + ); + } + return null; + }; + const renderTimeCard = (slotIndex) => { let maxConsecutive = 0; let roomId = ""; diff --git a/my-app/src/components/ui/TimeCardContainer.module.css b/my-app/src/components/ui/TimeCardContainer.module.css index 2a7519c..56c82fa 100644 --- a/my-app/src/components/ui/TimeCardContainer.module.css +++ b/my-app/src/components/ui/TimeCardContainer.module.css @@ -54,7 +54,7 @@ .column { min-width: 300px; - width: fit-content; + width: 350px; display: flex; flex-direction: column; gap: 0.5rem; diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 5afc620..2ccf11b 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -60,8 +60,9 @@ export function NewBooking({ addBooking }) { return ( + - + {/* Litet grupprum */}
diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index bfe1c80..77cc7f3 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -5,6 +5,14 @@ min-height: 100vh; } +.roomImage { + width: 100%; + height: auto; + margin-bottom: 1rem; + object-fit: cover; + aspect-ratio: 7/3; +} + .formContainer { /*padding: 1rem;*/ min-height: 100%; @@ -26,7 +34,6 @@ } .bookingTimesContainer { - margin-top: 2rem; border-radius: 0.3rem; outline: 1px solid var(--border-light); display: flex; diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index f22e73f..c0cb70a 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -13,7 +13,7 @@ border: 1px solid var(--border-light); position: sticky; width: 100%; - top: 1rem; + top: 0; z-index: 10; display: flex; flex-direction: row; -- 2.39.5 From 2c943cdb20064237bb4376afd36278b768db9faf Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:15:44 +0200 Subject: [PATCH 04/20] booking flow improvments --- .../InlineModalExtendedBookingForm.jsx | 24 +- my-app/src/components/layout/PageHeader.jsx | 11 +- .../components/layout/PageHeader.module.css | 30 ++- my-app/src/components/ui/Breadcrumbs.jsx | 32 +++ .../src/components/ui/Breadcrumbs.module.css | 40 ++++ .../src/components/ui/TimeCardContainer.jsx | 13 +- my-app/src/context/SettingsContext.jsx | 3 + my-app/src/pages/BookingSettings.jsx | 25 ++ my-app/src/pages/NewBooking.jsx | 215 +++++++++++++----- my-app/src/pages/NewBooking.module.css | 38 +++- my-app/src/pages/RoomBooking.jsx | 64 +++--- .../src/react-aria-starter/src/DatePicker.css | 2 +- my-app/src/styles/variables.css | 2 +- 13 files changed, 387 insertions(+), 112 deletions(-) create mode 100644 my-app/src/components/ui/Breadcrumbs.jsx create mode 100644 my-app/src/components/ui/Breadcrumbs.module.css diff --git a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx index 23c29db..5407c7c 100644 --- a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx +++ b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx @@ -144,9 +144,12 @@ export function InlineModalExtendedBookingForm({ // Check if user has selected an end time (including pre-selected) const hasSelectedEndTime = selectedEndTimeIndex !== null; + + // Check if user has added at least one additional participant (beyond themselves) + const hasRequiredParticipants = booking.participants.length > 0; const handleSave = () => { - if (hasSelectedEndTime && addBooking) { + if (hasSelectedEndTime && hasRequiredParticipants && addBooking) { console.log('Booking context state:', { title: booking.title, participants: booking.participants, @@ -306,6 +309,15 @@ export function InlineModalExtendedBookingForm({ {/* Participants Field - Compact */}
+ {/*!hasRequiredParticipants && ( +
+ 💡 Lägg till minst en deltagare +
+ )*/}
@@ -317,11 +329,13 @@ export function InlineModalExtendedBookingForm({ Avbryt
diff --git a/my-app/src/components/layout/PageHeader.jsx b/my-app/src/components/layout/PageHeader.jsx index 233e54c..1152520 100644 --- a/my-app/src/components/layout/PageHeader.jsx +++ b/my-app/src/components/layout/PageHeader.jsx @@ -1,15 +1,22 @@ import React from 'react'; import styles from './PageHeader.module.css'; -const PageHeader = ({ title, subtitle, imageUrl }) => { +const PageHeader = ({ title, subtitle, imageUrl, breadcrumbs }) => { + const headerClass = `${styles.header} ${imageUrl ? styles.withImage : styles.withoutImage}`; + return ( -
+
{imageUrl && (
{title}
)}
+ {breadcrumbs && ( +
+ {breadcrumbs} +
+ )}

{title}

{subtitle &&
{subtitle}
}
diff --git a/my-app/src/components/layout/PageHeader.module.css b/my-app/src/components/layout/PageHeader.module.css index cc37aee..6a4905f 100644 --- a/my-app/src/components/layout/PageHeader.module.css +++ b/my-app/src/components/layout/PageHeader.module.css @@ -1,6 +1,6 @@ .header { - margin-bottom: var(--spacing-2xl); - padding-bottom: var(--spacing-xl); + /*margin-bottom: var(--spacing-xl);*/ + padding: var(--spacing-2xl); border-bottom: 1px solid var(--border-light); display: flex; flex-direction: column; @@ -20,13 +20,16 @@ .textContent { flex: 1; - padding: 1rem; +} + +.breadcrumbsContainer { + margin-bottom: var(--spacing-md); } .pageHeading { color: var(--text-primary); - margin: 0 0 var(--spacing-md) 0; - font-size: 2.5rem; + margin: 0; + font-size: 2rem; font-weight: var(--font-weight-bold); line-height: 1.2; } @@ -35,4 +38,21 @@ color: var(--text-secondary); font-size: 1.1rem; font-weight: var(--font-weight-medium); +} + +/* Variant with image */ +.withImage { + /* Default styles for image variant */ + padding: 0; +} + +.withImage .textContent { + padding: var(--spacing-2xl); + padding-top: var(--spacing-md); +} + +@media (min-width: 750px) { + .image { + aspect-ratio: 11/3; + } } \ No newline at end of file diff --git a/my-app/src/components/ui/Breadcrumbs.jsx b/my-app/src/components/ui/Breadcrumbs.jsx new file mode 100644 index 0000000..7d827ff --- /dev/null +++ b/my-app/src/components/ui/Breadcrumbs.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import styles from './Breadcrumbs.module.css'; + +const Breadcrumbs = ({ items }) => { + return ( + + ); +}; + +export default Breadcrumbs; \ No newline at end of file diff --git a/my-app/src/components/ui/Breadcrumbs.module.css b/my-app/src/components/ui/Breadcrumbs.module.css new file mode 100644 index 0000000..545614a --- /dev/null +++ b/my-app/src/components/ui/Breadcrumbs.module.css @@ -0,0 +1,40 @@ +.breadcrumbs { + margin-bottom: 0; +} + +.breadcrumbList { + display: flex; + align-items: center; + list-style: none; + margin: 0; + padding: 0; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.breadcrumbItem { + display: flex; + align-items: center; +} + +.breadcrumbLink { + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s ease; +} + +.breadcrumbLink:hover { + color: var(--text-primary); + text-decoration: underline; +} + +.breadcrumbCurrent { + color: var(--text-primary); + font-weight: 500; +} + +.breadcrumbSeparator { + margin: 0 0.5rem; + color: var(--text-tertiary); + font-size: 0.75rem; +} \ No newline at end of file diff --git a/my-app/src/components/ui/TimeCardContainer.jsx b/my-app/src/components/ui/TimeCardContainer.jsx index 301c922..af149f7 100644 --- a/my-app/src/components/ui/TimeCardContainer.jsx +++ b/my-app/src/components/ui/TimeCardContainer.jsx @@ -11,7 +11,7 @@ import modalStyles from '../booking/BookingModal.module.css'; import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; -export function TimeCardContainer({ addBooking }) { +export function TimeCardContainer({ addBooking, forceOneColumn = false }) { const navigate = useNavigate(); const booking = useBookingContext(); const { settings } = useSettingsContext(); @@ -79,6 +79,11 @@ export function TimeCardContainer({ addBooking }) { function slotIndiciesToColumns(originalArray) { const width = window.innerWidth; + // Force single column if forceOneColumn prop is true + if (forceOneColumn) { + return [originalArray]; + } + if (width >= 769 && width <= LARGE_BREAKPOINT) { // For medium screens: group in pairs first, then distribute pairs across 2 columns const pairs = []; @@ -109,7 +114,7 @@ export function TimeCardContainer({ addBooking }) { const renderColumn = (column, columnIndex) => { const width = window.innerWidth; - if (width >= 769 && width <= LARGE_BREAKPOINT) { + if (width >= 769 && width <= LARGE_BREAKPOINT && !forceOneColumn) { // For medium screens: render pairs in rows const pairs = []; for (let i = 0; i < column.length; i += 2) { @@ -144,8 +149,8 @@ export function TimeCardContainer({ addBooking }) { }).flat()}
); - } else if (width < 769) { - // For mobile: render pairs with spacing between every 4 pairs + } else if (width < 769 || forceOneColumn) { + // For mobile or forced single column: render pairs with spacing between every 4 pairs const pairs = []; for (let i = 0; i < column.length; i += 2) { pairs.push([column[i], column[i + 1]]); diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 4cba687..9fa5a99 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -33,6 +33,7 @@ export const SettingsProvider = ({ children }) => { showBookingDeleteBanner: false, bookingFormType: 'inline', // 'modal' or 'inline' showFiltersAlways: false, // Show filter dropdowns always or behind toggle button + newBookingLayoutVariant: false, // false = stacked, true = side-by-side // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -70,6 +71,8 @@ export const SettingsProvider = ({ children }) => { bookingFormType: 'inline', // 'modal' or 'inline' // Filter display mode showFiltersAlways: false, // Show filter dropdowns always or behind toggle button + // New booking page layout variant + newBookingLayoutVariant: false, // false = stacked, true = side-by-side }; }); diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 73e2406..ce196e9 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -196,6 +196,31 @@ export function BookingSettings() { Show Filter Button: Filters are hidden behind a collapsible button
+ +
+ +
+ updateSettings({ newBookingLayoutVariant: e.target.checked })} + className={styles.toggle} + /> + + {settings.newBookingLayoutVariant ? 'Side-by-Side' : 'Stacked'} + +
+
+ Stacked: Image and header at top, booking times below
+ Side-by-Side: Image/header on left, booking times on right (medium screens) +
+
diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 2ccf11b..e05b0cd 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -9,6 +9,7 @@ import { BookingProvider } from '../context/BookingContext'; import { useSettingsContext } from '../context/SettingsContext'; import PageHeader from '../components/layout/PageHeader'; import PageContainer from '../components/layout/PageContainer'; +import Breadcrumbs from '../components/ui/Breadcrumbs'; export function NewBooking({ addBooking }) { const { getEffectiveToday, settings } = useSettingsContext(); @@ -58,75 +59,167 @@ export function NewBooking({ addBooking }) { setShowFilters(false); }; + const breadcrumbItems = [ + { label: 'Lokalbokning', path: '/' }, + { label: 'Ny bokning', path: '/new-booking' } + ]; + return ( - - - {/* Litet grupprum */} -
-
- -
- - - {/* Filter Section */} -
-
- {settings.showFiltersAlways ? ( - /* Always-visible filters */ -
-
- - -
-
- ) : ( - /* Toggle button with collapsible filters */ - <> - +
+ {settings.newBookingLayoutVariant ? ( + /* Side-by-side layout */ + <> +
+ } + /> +
+
+ +
+
+
+ - {/* Collapsible Filter Content */} - {showFilters && ( -
-
- - -
- {hasActiveFilters && ( -
- + {/* Filter Section */} +
+
+ {settings.showFiltersAlways ? ( + /* Always-visible filters */ +
+
+ + +
+ ) : ( + /* Toggle button with collapsible filters */ + <> + + + {/* Collapsible Filter Content */} + {showFilters && ( +
+
+ + +
+ {hasActiveFilters && ( +
+ +
+ )} +
+ )} + )}
- )} - - )} +

+ Välj starttid +

+
+ +
+ +
+
+
-

- Välj starttid -

-
- -
- -
+
-
-
-
+ + ) : ( + /* Stacked layout (original) */ + <> + } + /> +
+
+ +
+ + + {/* Filter Section */} +
+
+ {settings.showFiltersAlways ? ( + /* Always-visible filters */ +
+
+ + +
+
+ ) : ( + /* Toggle button with collapsible filters */ + <> + + + {/* Collapsible Filter Content */} + {showFilters && ( +
+
+ + +
+ {hasActiveFilters && ( +
+ +
+ )} +
+ )} + + )} +
+

+ Välj starttid +

+
+ +
+ +
+
+
+
+ + )} +
); } \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index 77cc7f3..5d3ee77 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -1,5 +1,6 @@ + .pageContainer { - padding: var(--container-padding); + /*padding: var(--container-padding);*/ background-color: var(--bg-primary); color: var(--text-primary); min-height: 100vh; @@ -39,7 +40,7 @@ display: flex; flex-direction: column; align-items: center; - background-color: var(--bg-secondary); + background-color: var(--bg-primary); padding-bottom: 2rem; } @@ -300,4 +301,37 @@ opacity: 1; transform: translateY(0); } +} + +/* Side-by-side layout variant */ +.newBookingPageContainer { + /*padding: var(--spacing-lg);*/ +} + +.newBookingPageContainer.sideBySide { + display: flex; + flex-direction: column; +} + +@media (min-width: 769px) and (max-width: 1400px) { + .newBookingPageContainer.sideBySide { + display: flex; + flex-direction: row; + gap: 2rem; + align-items: flex-start; + } + + .newBookingPageContainer.sideBySide .headerSection { + flex: 0 0 400px; + max-width: 400px; + } + + .newBookingPageContainer.sideBySide .contentSection { + flex: 1; + min-width: 0; + } + + .newBookingPageContainer.sideBySide .bookingTimesContainer { + margin-top: 0; + } } \ No newline at end of file diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 9a6cc57..b86ccd4 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -6,6 +6,7 @@ import Card from '../components/ui/Card'; import { useSettingsContext } from '../context/SettingsContext'; import { USER } from '../constants/bookingConstants'; import PageHeader from '../components/layout/PageHeader'; +import PageContainer from '../components/layout/PageContainer'; export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) { const { settings } = useSettingsContext(); @@ -24,7 +25,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o } return ( -
+ <> {isTestSessionActive && (
@@ -34,37 +35,38 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
)} - -

Ny bokning

+ +

Ny bokning

-
- - - - - - -
-
+
+ + + + + + +
+
-
-

Mina bokingar

- -
-
+
+

Mina bokingar

+ +
+ + ); } \ No newline at end of file diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index c0cb70a..2f95bfd 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -10,7 +10,7 @@ color: var(--text-primary); background-color: var(--bg-secondary); padding: var(--spacing-md); - border: 1px solid var(--border-light); + border-bottom: 1px solid var(--border-light); position: sticky; width: 100%; top: 0; diff --git a/my-app/src/styles/variables.css b/my-app/src/styles/variables.css index b2c8ca7..18e1d2d 100644 --- a/my-app/src/styles/variables.css +++ b/my-app/src/styles/variables.css @@ -298,7 +298,7 @@ /* Button disabled states */ --button-disabled-bg: #f8f9fa; - --button-disabled-text: #adb5bd; + --button-disabled-text: #797e83; --button-disabled-border: #dee2e6; /* Additional color variants */ -- 2.39.5 From 06bb36e80eebf751a1b6ea1a77883b0f2541ecfb Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:22:57 +0200 Subject: [PATCH 05/20] end of day --- my-app/src/AppRoutes.jsx | 21 ++ my-app/src/components/booking/BookingCard.jsx | 263 ++++++++++++------ .../components/booking/BookingCard.module.css | 106 +++++-- .../src/components/booking/BookingsList.jsx | 48 +++- .../booking/BookingsList.module.css | 18 ++ 5 files changed, 337 insertions(+), 119 deletions(-) diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index e86a380..8bc5511 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -33,6 +33,7 @@ const AppRoutes = () => { roomCategory: 'green', title: 'Team standup', participants: [ + { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, { id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' }, { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, { id: 4, name: 'Elin Rudling', username: 'elru4521', email: 'elin.rudling@dsv.su.se' } @@ -47,6 +48,7 @@ const AppRoutes = () => { roomCategory: 'red', title: 'Project planning workshop', participants: [ + { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, { id: 6, name: 'Ellen Britschgi', username: 'elbr5623', email: 'ellen.britschgi@dsv.su.se' }, { id: 7, name: 'Anna Andersson', username: 'anan3457', email: 'anna.andersson@dsv.su.se' }, @@ -64,6 +66,7 @@ const AppRoutes = () => { roomCategory: 'blue', title: '1:1 with supervisor', participants: [ + { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' } ] }, @@ -76,9 +79,27 @@ const AppRoutes = () => { roomCategory: 'yellow', title: 'Study group session', participants: [ + { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, { id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' }, { id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' } ] + }, + { + id: 5, + date: new CalendarDate(2025, 9, 7), + startTime: 10, + endTime: 12, + room: 'G5:9', + roomCategory: 'blue', + title: 'Design review meeting', + createdBy: { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, + participants: [ + { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, + { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, + { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' } + ], + isParticipantBooking: true } ]); diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index 2dadc87..bf1e814 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -6,9 +6,11 @@ import Dropdown from '../ui/Dropdown'; import { BookingTitleField } from '../forms/BookingTitleField'; import { ParticipantsSelector } from '../forms/ParticipantsSelector'; import { BookingProvider } from '../../context/BookingContext'; -import { PEOPLE } from '../../constants/bookingConstants'; +import { PEOPLE, USER } from '../../constants/bookingConstants'; function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) { + // Check if this is a participant booking (user was added by someone else) + const isParticipantBooking = booking.isParticipantBooking === true; const [selectedLength, setSelectedLength] = useState(null); const [calculatedEndTime, setCalculatedEndTime] = useState(null); const [editedTitle, setEditedTitle] = useState(''); @@ -118,6 +120,14 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD setShowDeleteConfirm(false); } + function handleRemoveSelf() { + // For participant bookings, remove the current user from participants + // This effectively "leaves" the booking + if (isParticipantBooking && onBookingDelete) { + onBookingDelete(booking); + } + } + function getTimeFromIndex(timeIndex) { const totalHalfHoursFromStart = timeIndex; @@ -133,13 +143,35 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD function formatParticipants(participants) { if (!participants || participants.length === 0) return null; + const formatName = (participant, index) => { + // Bold the booker's name (creator in participant bookings, or current user in regular bookings) + const isBooker = isParticipantBooking + ? (booking.createdBy && participant.id === booking.createdBy.id) + : (participant.id === USER.id); + + const firstName = participant.name.split(' ')[0]; + + if (isBooker) { + return {firstName}; + } + return {firstName}; + }; + if (participants.length === 1) { - return participants[0].name; + return formatName(participants[0], 0); } else if (participants.length === 2) { - return `${participants[0].name} and ${participants[1].name}`; + return ( + <> + {formatName(participants[0], 0)} and {formatName(participants[1], 1)} + + ); } else { const remaining = participants.length - 2; - return `${participants[0].name}, ${participants[1].name} and ${remaining} more`; + return ( + <> + {formatName(participants[0], 0)}, {formatName(participants[1], 1)} and {remaining} more + + ); } } @@ -147,95 +179,166 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD
- {convertDateObjectToString(booking.date)}

{booking.title}

-
- {booking.room} - {booking.participants && booking.participants.length > 0 && ( -

{formatParticipants(booking.participants)}

- )} -
- + + {isParticipantBooking && booking.createdBy && ( +
+ Tillagd av {booking.createdBy.name} +
+ )} + + {!isParticipantBooking && booking.participants && booking.participants.length > 0 && ( +
{formatParticipants(booking.participants)}
+ )}
-
-
{getTimeFromIndex(booking.startTime)}
-
{getTimeFromIndex(calculatedEndTime || booking.endTime)}
+
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} +
+
+ {booking.room} +
{isExpanded && (
-
- -
- -
- -
- -
- - -
- - {!showDeleteConfirm ? ( -
- - - -
+ {isParticipantBooking ? ( + // Participant booking view - read-only with remove self option + <> +
+
+ +

{booking.createdBy?.name}

+
+
+ +

+ {booking.participants + ?.filter(p => p.id !== booking.createdBy?.id) + ?.map(p => p.name) + ?.join(', ')} +

+
+
+ + {!showDeleteConfirm ? ( +
+ + +
+ ) : ( +
+
+ ⚠️ +

Är du säker på att du vill lämna denna bokning?

+

+ Du kommer inte längre att vara med på "{booking.title}" den {booking.date.day}/{booking.date.month} +

+
+
+ + +
+
+ )} + ) : ( -
-
- ⚠️ -

Är du säker på att du vill radera denna bokning?

-

- "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} -

+ // Regular booking view - editable + <> +
+
-
- - + +
+
-
+ +
+ + +
+ + {!showDeleteConfirm ? ( +
+ + + +
+ ) : ( +
+
+ ⚠️ +

Är du säker på att du vill radera denna bokning?

+

+ "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} +

+
+
+ + +
+
+ )} + )}
diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 09f3df7..f9c6c4b 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -1,56 +1,83 @@ .card { border: var(--border-width-thin) solid var(--border-light); - padding: var(--card-padding); + padding: 1rem; width: 100%; border-radius: var(--border-radius-md); background: var(--bg-primary); transition: var(--transition-medium); - box-shadow: var(--shadow-lg); + /*box-shadow: var(--shadow-lg);*/ } -.card:hover { - cursor: pointer; - border-color: var(--color-primary); - box-shadow: var(--shadow-xl); - /*transform: translateY(-2px);*/ +@media (hover: hover) { + .card:hover { + cursor: pointer; + border-color: var(--color-primary); + box-shadow: var(--shadow-xl); + /*transform: translateY(-2px);*/ + } } + .header { display: flex; justify-content: space-between; - align-items: center; - height: 100%; + align-items: flex-start; + gap: 1rem; + width: 100%; } .leftSection { flex: 1; -} - -.date { - text-transform: uppercase; - font-size: var(--font-size-sm); - font-weight: var(--font-weight-semibold); - color: var(--text-muted); - letter-spacing: 1px; - margin-bottom: var(--spacing-sm); - display: block; -} - -.titleRow { display: flex; - align-items: center; - gap: var(--spacing-md); - margin-bottom: var(--spacing-sm); + flex-direction: column; + gap: 0.5rem; + height: 100%; +} + +.rightSection { + display: flex; + flex-direction: column; + align-items: flex-end; + align-items: end +} + +.createdByIndicator { + font-size: var(--font-size-sm); + color: var(--text-muted); + font-weight: var(--font-weight-normal); + font-style: italic; } .title { margin: 0; - font-size: var(--font-size-3xl); - font-weight: var(--font-weight-bold); + font-size: 1.1rem; + font-weight: 600; color: var(--text-primary); line-height: 1.3; } +.time { + font-size: 1.1rem; + font-weight: 600; + color: #111827; + white-space: nowrap; +} + +.createdBy { + font-size: 0.875rem; + color: #6b7280; +} + +.roomBadge { + background-color: #065f46; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 10rem; + font-size: 1rem; + font-weight: 500; + white-space: nowrap; +} + .room { font-weight: var(--font-weight-semibold); font-size: var(--font-size-xl); @@ -89,8 +116,8 @@ .participants { margin: 0; - font-size: var(--font-size-md); - color: var(--text-muted); + font-size: 1rem; + color: #6b7280; } /* Expanded card styles */ @@ -117,6 +144,27 @@ margin-bottom: 1.5rem; } +.readOnlySection { + margin-bottom: 1.5rem; +} + +.readOnlyField { + margin-bottom: 1rem; +} + +.readOnlyField:last-child { + margin-bottom: 0; +} + +.createdByText, +.participantsText, +.timeText { + margin: 0; + font-size: var(--font-size-base); + color: var(--text-primary); + font-weight: var(--font-weight-medium); +} + .editSection { margin-bottom: 1.5rem; } diff --git a/my-app/src/components/booking/BookingsList.jsx b/my-app/src/components/booking/BookingsList.jsx index 369c426..7579e30 100644 --- a/my-app/src/components/booking/BookingsList.jsx +++ b/my-app/src/components/booking/BookingsList.jsx @@ -15,10 +15,31 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD return dateA - dateB; }); + // Group bookings by date + const groupedBookings = sortedBookings.reduce((groups, booking) => { + const dateKey = `${booking.date.year}-${booking.date.month}-${booking.date.day}`; + if (!groups[dateKey]) { + groups[dateKey] = []; + } + groups[dateKey].push(booking); + return groups; + }, {}); + function handleBookingClick(booking) { setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id); } + function formatDateHeader(dateObj) { + const days = ['SÖNDAG', 'MÅNDAG', 'TISDAG', 'ONSDAG', 'TORSDAG', 'FREDAG', 'LÖRDAG']; + const months = ['JANUARI', 'FEBRUARI', 'MARS', 'APRIL', 'MAJ', 'JUNI', 'JULI', 'AUGUSTI', 'SEPTEMBER', 'OKTOBER', 'NOVEMBER', 'DECEMBER']; + + const date = new Date(dateObj.year, dateObj.month - 1, dateObj.day); + const dayName = days[date.getDay()]; + const monthName = months[dateObj.month - 1]; + + return `${dayName} ${dateObj.day} ${monthName} ${dateObj.year}`; + } + return (
{showSuccessBanner && ( @@ -71,17 +92,24 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD /> )}
- {sortedBookings.length > 0 ? ( + {Object.keys(groupedBookings).length > 0 ? ( <> - {sortedBookings.map((booking, index) => ( - handleBookingClick(booking)} - isExpanded={expandedBookingId === booking.id} - onBookingUpdate={onBookingUpdate} - onBookingDelete={onBookingDelete} - /> + {Object.entries(groupedBookings).map(([dateKey, dayBookings]) => ( +
+

+ {formatDateHeader(dayBookings[0].date)} +

+ {dayBookings.map((booking, index) => ( + handleBookingClick(booking)} + isExpanded={expandedBookingId === booking.id} + onBookingUpdate={onBookingUpdate} + onBookingDelete={onBookingDelete} + /> + ))} +
))} ) : ( diff --git a/my-app/src/components/booking/BookingsList.module.css b/my-app/src/components/booking/BookingsList.module.css index 7910180..1219d7e 100644 --- a/my-app/src/components/booking/BookingsList.module.css +++ b/my-app/src/components/booking/BookingsList.module.css @@ -63,4 +63,22 @@ opacity: 1; transform: translateY(0); } +} + +.dateGroup { + margin-bottom: 2rem; +} + +.dateGroup:last-child { + margin-bottom: 0; +} + +.dateHeader { + font-size: 0.875rem; + font-weight: 600; + color: #9ca3af; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 0.25rem 0; + padding: 0; } \ No newline at end of file -- 2.39.5 From bfe8128d0f4ad58ded76afc743165f7538879681 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:52:11 +0200 Subject: [PATCH 06/20] booking card final design! --- my-app/src/components/booking/BookingCard.jsx | 14 ++-- .../components/booking/BookingCard.module.css | 64 +++++++------------ 2 files changed, 30 insertions(+), 48 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index bf1e814..06b83bc 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -178,11 +178,16 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD return (
-
+

{booking.title}

- +
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} +
+ +
+
{isParticipantBooking && booking.createdBy && (
Tillagd av {booking.createdBy.name} @@ -192,11 +197,6 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD {!isParticipantBooking && booking.participants && booking.participants.length > 0 && (
{formatParticipants(booking.participants)}
)} -
-
-
- {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} -
{booking.room}
diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index f9c6c4b..2230c93 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -1,11 +1,9 @@ .card { border: var(--border-width-thin) solid var(--border-light); - padding: 1rem; + padding: 0.8rem 1rem; width: 100%; - border-radius: var(--border-radius-md); background: var(--bg-primary); transition: var(--transition-medium); - /*box-shadow: var(--shadow-lg);*/ } @media (hover: hover) { @@ -20,25 +18,26 @@ .header { display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1rem; + flex-direction: column; + gap: 0.2rem; width: 100%; } -.leftSection { - flex: 1; +.topSection { display: flex; - flex-direction: column; - gap: 0.5rem; - height: 100%; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; + /*background-color: lightblue;*/ } -.rightSection { +.bottomSection { display: flex; - flex-direction: column; + flex-direction: row; + justify-content: space-between; align-items: flex-end; - align-items: end + width: 100%; + /*background-color: lightgreen;*/ } .createdByIndicator { @@ -68,11 +67,18 @@ color: #6b7280; } +.participants { + margin: 0; + font-size: 1rem; + color: #6b7280; + padding: 0.25rem 0; +} + .roomBadge { - background-color: #065f46; + background-color: var(--su-blue); color: white; padding: 0.25rem 0.75rem; - border-radius: 10rem; + /*border-radius: 10rem;*/ font-size: 1rem; font-weight: 500; white-space: nowrap; @@ -92,32 +98,8 @@ gap: var(--spacing-lg); } -.timeSection { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 0.2rem; -} - -.startTime { - font-size: 1.6rem; - font-weight: var(--font-weight-normal); +.time { color: var(--text-primary); - line-height: 1; -} - -.endTime { - font-size: 1.6rem; - font-weight: var(--font-weight-normal); - color: var(--text-tertiary); - line-height: 1; -} - -.participants { - margin: 0; - font-size: 1rem; - color: #6b7280; } /* Expanded card styles */ -- 2.39.5 From a24b5251705d61bd3c413bd3082278a2e1b0a48b Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:18:32 +0200 Subject: [PATCH 07/20] booking card final i think and hope --- my-app/src/components/booking/BookingCard.jsx | 302 +++++++++--------- .../components/booking/BookingCard.module.css | 42 ++- .../components/forms/BookingTitleField.jsx | 3 +- .../components/forms/ParticipantsSelector.jsx | 3 +- .../layout/PageContainer.module.css | 6 + my-app/src/components/ui/Label.jsx | 10 + my-app/src/components/ui/Label.module.css | 9 + 7 files changed, 214 insertions(+), 161 deletions(-) create mode 100644 my-app/src/components/ui/Label.jsx create mode 100644 my-app/src/components/ui/Label.module.css diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index 06b83bc..41b7e04 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -6,6 +6,7 @@ import Dropdown from '../ui/Dropdown'; import { BookingTitleField } from '../forms/BookingTitleField'; import { ParticipantsSelector } from '../forms/ParticipantsSelector'; import { BookingProvider } from '../../context/BookingContext'; +import { Label } from '../ui/Label'; import { PEOPLE, USER } from '../../constants/bookingConstants'; function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) { @@ -176,173 +177,176 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD } return ( -
-
-
-
-

{booking.title}

-
-
- {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} -
- -
-
- {isParticipantBooking && booking.createdBy && ( -
- Tillagd av {booking.createdBy.name} +
+
+
+
+
+

{booking.title}

+
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} +
+ +
+
+ {booking.participants && booking.participants.length > 0 && ( +
{formatParticipants(booking.participants)}
+ )} +
+ {booking.room}
- )} - - {!isParticipantBooking && booking.participants && booking.participants.length > 0 && ( -
{formatParticipants(booking.participants)}
- )} -
- {booking.room}
-
- - {isExpanded && ( - + {isExpanded && (
- {isParticipantBooking ? ( - // Participant booking view - read-only with remove self option - <> -
-
- -

{booking.createdBy?.name}

-
-
- -

- {booking.participants - ?.filter(p => p.id !== booking.createdBy?.id) - ?.map(p => p.name) - ?.join(', ')} -

-
-
- - {!showDeleteConfirm ? ( -
- - -
- ) : ( -
-
- ⚠️ -

Är du säker på att du vill lämna denna bokning?

-

- Du kommer inte längre att vara med på "{booking.title}" den {booking.date.day}/{booking.date.month} + + {isParticipantBooking ? ( + // Participant booking view - read-only with remove self option + <> +

+
+ +

{booking.createdBy?.name}

+
+
+ +

+ {booking.participants + ?.filter(p => p.id !== booking.createdBy?.id) + ?.map(p => p.name) + ?.join(', ')}

-
+
+ + {!showDeleteConfirm ? ( +
+
+ ) : ( +
+
+ ⚠️ +

Är du säker på att du vill lämna denna bokning?

+

+ Du kommer inte längre att vara med på "{booking.title}" den {booking.date.day}/{booking.date.month} +

+
+
+ + +
+
+ )} + + ) : ( + // Regular booking view - editable + <> +
+ +
+ +
+ +
+ +
+ + +
+ + {!showDeleteConfirm ? ( +
+ + -
-
- )} - - ) : ( - // Regular booking view - editable - <> -
- -
- -
- -
- -
- - -
- - {!showDeleteConfirm ? ( -
- - - -
- ) : ( -
-
- ⚠️ -

Är du säker på att du vill radera denna bokning?

-

- "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} -

-
-
-
-
- )} - - )} + ) : ( +
+
+ ⚠️ +

Är du säker på att du vill radera denna bokning?

+

+ "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} +

+
+
+ + +
+
+ )} + + )} +
- - )} + )} + +
+ {isParticipantBooking && booking.createdBy && !isExpanded && ( +
+ Tillagd av {booking.createdBy.name} +
+ )} + +
); } diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 2230c93..67fb199 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -1,3 +1,8 @@ +.cardWrapper { + width: 100%; + transition: var(--transition-medium); +} + .card { border: var(--border-width-thin) solid var(--border-light); padding: 0.8rem 1rem; @@ -7,7 +12,7 @@ } @media (hover: hover) { - .card:hover { + .cardWrapper:hover .card { cursor: pointer; border-color: var(--color-primary); box-shadow: var(--shadow-xl); @@ -15,11 +20,22 @@ } } +.banner { + background-color: #f3f4f6; + color: #6b7280; + font-size: 0.8rem; + padding: 0.25rem 1rem; + border-left: var(--border-width-thin) solid var(--border-light); + border-right: var(--border-width-thin) solid var(--border-light); + border-bottom: var(--border-width-thin) solid var(--border-light); + /*border-bottom-left-radius: var(--border-radius-md); + border-bottom-right-radius: var(--border-radius-md);*/ +} + .header { display: flex; flex-direction: column; - gap: 0.2rem; width: 100%; } @@ -62,12 +78,8 @@ white-space: nowrap; } +.participants, .createdBy { - font-size: 0.875rem; - color: #6b7280; -} - -.participants { margin: 0; font-size: 1rem; color: #6b7280; @@ -117,8 +129,8 @@ } .expandedContent { - margin-top: 1.5rem; - padding-top: 1.5rem; + padding-top:1rem; + margin-top:1rem; border-top: 1px solid #E5E5E5; } @@ -156,7 +168,17 @@ font-size: 0.8rem; color: #717171; font-weight: 500; - margin-bottom: 0.5rem; + /*margin-bottom: 0.5rem;*/ + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.compactElementHeading { + font-size: 0.75rem; + color: var(--text-tertiary); + font-weight: 500; + margin-bottom: 0.4rem; + margin-top: 0; text-transform: uppercase; letter-spacing: 0.5px; } diff --git a/my-app/src/components/forms/BookingTitleField.jsx b/my-app/src/components/forms/BookingTitleField.jsx index efc1e3c..cbb261d 100644 --- a/my-app/src/components/forms/BookingTitleField.jsx +++ b/my-app/src/components/forms/BookingTitleField.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; +import { Label } from '../ui/Label'; import styles from './BookingTitleField.module.css'; export function BookingTitleField({ compact = false, hideLabel = false }) { @@ -10,7 +11,7 @@ export function BookingTitleField({ compact = false, hideLabel = false }) { return ( <> {!hideLabel && ( -

Titel på bokning

+ )} {!hideLabel && ( -

Deltagare

+ )} {/* Search Input */} diff --git a/my-app/src/components/layout/PageContainer.module.css b/my-app/src/components/layout/PageContainer.module.css index d81e635..205bf04 100644 --- a/my-app/src/components/layout/PageContainer.module.css +++ b/my-app/src/components/layout/PageContainer.module.css @@ -3,4 +3,10 @@ margin: 0 auto; padding: var(--spacing-2xl); min-height: 100vh; +} + +@media (max-width: 768px) { + .pageContainer { + padding: var(--spacing-lg); + } } \ No newline at end of file diff --git a/my-app/src/components/ui/Label.jsx b/my-app/src/components/ui/Label.jsx new file mode 100644 index 0000000..6d1f67d --- /dev/null +++ b/my-app/src/components/ui/Label.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import styles from './Label.module.css'; + +export function Label({ children, className = '', ...props }) { + return ( +

+ {children} +

+ ); +} \ No newline at end of file diff --git a/my-app/src/components/ui/Label.module.css b/my-app/src/components/ui/Label.module.css new file mode 100644 index 0000000..03f8423 --- /dev/null +++ b/my-app/src/components/ui/Label.module.css @@ -0,0 +1,9 @@ +.label { + font-size: 0.75rem; + color: var(--text-tertiary); + font-weight: 500; + margin-bottom: 0.4rem; + margin-top: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} \ No newline at end of file -- 2.39.5 From 15e72f77db71ac1928f9a68bb03b840ba9a68564 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:41:00 +0200 Subject: [PATCH 08/20] booking card fixes --- my-app/src/components/booking/BookingCard.jsx | 76 ++++++++++--------- .../components/booking/BookingCard.module.css | 11 ++- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index 41b7e04..c930c1b 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -51,42 +51,51 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD } }; - const bookingLengths = [ - { value: 1, label: "30 min" }, - { value: 2, label: "1 h" }, - { value: 3, label: "1.5 h" }, - { value: 4, label: "2 h" }, - { value: 5, label: "2.5 h" }, - { value: 6, label: "3 h" }, - { value: 7, label: "3.5 h" }, - { value: 8, label: "4 h" }, - ]; + const bookingLengths = []; + for (let i = 1; i <= hoursAvailable; i++) { + const endTimeIndex = booking.startTime + i; + const endTime = getTimeFromIndex(endTimeIndex); + const durationLabel = i === 1 ? "30 min" : + i === 2 ? "1 h" : + i === 3 ? "1.5 h" : + i === 4 ? "2 h" : + i === 5 ? "2.5 h" : + i === 6 ? "3 h" : + i === 7 ? "3.5 h" : + i === 8 ? "4 h" : `${i * 0.5} h`; + + bookingLengths.push({ + value: endTimeIndex, + label: `${endTime} · ${durationLabel}` + }); + } - const disabledOptions = { - 1: !(hoursAvailable > 0), - 2: !(hoursAvailable > 1), - 3: !(hoursAvailable > 2), - 4: !(hoursAvailable > 3), - 5: !(hoursAvailable > 4), - 6: !(hoursAvailable > 5), - 7: !(hoursAvailable > 6), - 8: !(hoursAvailable > 7), - }; + // No disabled options needed since we only generate available options + const disabledOptions = {}; function handleLengthChange(event) { - const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); - setSelectedLength(lengthValue); + const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value); + setCalculatedEndTime(endTimeValue); - if (lengthValue !== null) { - const newEndTime = booking.startTime + lengthValue; - setCalculatedEndTime(newEndTime); + if (endTimeValue !== null) { + const newLength = endTimeValue - booking.startTime; + setSelectedLength(newLength); } else { + setSelectedLength(currentLength); setCalculatedEndTime(booking.endTime); } } + // Check if any changes have been made + const hasChanges = () => { + const titleChanged = editedTitle !== booking.title; + const participantsChanged = JSON.stringify(editedParticipants) !== JSON.stringify(booking.participants || []); + const endTimeChanged = calculatedEndTime !== booking.endTime; + return titleChanged || participantsChanged || endTimeChanged; + }; + function handleSave() { - if (selectedLength !== null && onBookingUpdate) { + if (hasChanges() && onBookingUpdate) { const updatedBooking = { ...booking, title: editedTitle, @@ -278,14 +287,13 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD options={bookingLengths} disabledOptions={disabledOptions} onChange={handleLengthChange} - value={selectedLength || ""} - placeholder={{ - value: "", - label: "Välj bokningslängd" - }} + value={calculatedEndTime || booking.endTime} + placeholder={null} />
+
+ {!showDeleteConfirm ? (
) : ( diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 67fb199..5a441dc 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -14,8 +14,8 @@ @media (hover: hover) { .cardWrapper:hover .card { cursor: pointer; - border-color: var(--color-primary); - box-shadow: var(--shadow-xl); + border-color: var(--border-medium); + /*box-shadow: var(--shadow-xl);*/ /*transform: translateY(-2px);*/ } } @@ -134,6 +134,13 @@ border-top: 1px solid #E5E5E5; } +.divider { + border: none; + border-top: 1px solid var(--border-medium); + margin: 1rem 0; + margin-bottom: 1.5rem; +} + .formSection { margin-bottom: 1.5rem; } -- 2.39.5 From 9bc68a63caad0a852a0fa19e7bbd82ae0d4542a1 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:33:27 +0200 Subject: [PATCH 09/20] responsiveness booking cards container --- .../components/booking/BookingCard.module.css | 2 ++ .../booking/BookingsList.module.css | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 5a441dc..15621b1 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -1,6 +1,8 @@ .cardWrapper { width: 100%; transition: var(--transition-medium); + /*max-width: 400px;*/ + flex: 1; } .card { diff --git a/my-app/src/components/booking/BookingsList.module.css b/my-app/src/components/booking/BookingsList.module.css index 1219d7e..c9eefa0 100644 --- a/my-app/src/components/booking/BookingsList.module.css +++ b/my-app/src/components/booking/BookingsList.module.css @@ -1,13 +1,16 @@ .bookingsListContainer { padding-bottom: var(--spacing-3xl); display: flex; - flex-direction: column; + flex-direction: row; + flex-wrap: wrap; } .bookingsContainer { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); + width: 100%; + display: grid; + grid-template-columns: repeat(2, 1fr); + flex-wrap: wrap; + column-gap: 2rem; } .message { @@ -67,6 +70,9 @@ .dateGroup { margin-bottom: 2rem; + flex: 1; + min-width: 250px; + max-width: 500px; } .dateGroup:last-child { @@ -74,6 +80,7 @@ } .dateHeader { + width: fit-content; font-size: 0.875rem; font-weight: 600; color: #9ca3af; @@ -81,4 +88,14 @@ letter-spacing: 0.05em; margin: 0 0 0.25rem 0; padding: 0; +} + +@media screen and (max-width: 768px) { + .bookingsContainer { + grid-template-columns: 1fr; + } + + .dateGroup { + max-width: 100%; + } } \ No newline at end of file -- 2.39.5 From 5f697015adedd45e2e1168ca92f672766a0479b5 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:41:35 +0200 Subject: [PATCH 10/20] color fixes --- my-app/src/components/booking/BookingCard.jsx | 4 ++-- my-app/src/components/booking/BookingCard.module.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index c930c1b..edd2a4d 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -161,9 +161,9 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD const firstName = participant.name.split(' ')[0]; - if (isBooker) { + /*if (isBooker) { return {firstName}; - } + }*/ return {firstName}; }; diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 15621b1..a476652 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -23,7 +23,7 @@ } .banner { - background-color: #f3f4f6; + background-color: var(--bg-secondary); color: #6b7280; font-size: 0.8rem; padding: 0.25rem 1rem; -- 2.39.5 From a8a52cb58456a25092ab0b9a3ed55d021b110f30 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:40:56 +0200 Subject: [PATCH 11/20] =?UTF-8?q?good=20enough=20f=C3=B6rhoppningsvis=20re?= =?UTF-8?q?sponsitivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my-app/src/components/booking/BookingCard.module.css | 12 +++++++++--- my-app/src/components/booking/BookingsList.jsx | 6 ++++++ .../src/components/booking/BookingsList.module.css | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index a476652..579cabe 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -2,7 +2,7 @@ width: 100%; transition: var(--transition-medium); /*max-width: 400px;*/ - flex: 1; + /*flex: 1;*/ } .card { @@ -10,11 +10,11 @@ padding: 0.8rem 1rem; width: 100%; background: var(--bg-primary); - transition: var(--transition-medium); + /*transition: var(--transition-medium);*/ } @media (hover: hover) { - .cardWrapper:hover .card { + .cardWrapper:hover:not(.expanded) .card { cursor: pointer; border-color: var(--border-medium); /*box-shadow: var(--shadow-xl);*/ @@ -122,6 +122,12 @@ box-shadow: var(--shadow-xl); } +.expanded .card { + outline: 2px solid #3b82f6; + outline-offset: -1px; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); +} + .expanded:hover { transform: none; } diff --git a/my-app/src/components/booking/BookingsList.jsx b/my-app/src/components/booking/BookingsList.jsx index 7579e30..690a9a6 100644 --- a/my-app/src/components/booking/BookingsList.jsx +++ b/my-app/src/components/booking/BookingsList.jsx @@ -22,9 +22,15 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD groups[dateKey] = []; } groups[dateKey].push(booking); + console.log("HEY:"); + console.log(groups); return groups; }, {}); + const groupedBookingsArray = Object.values(groupedBookings); + console.log("GROUPED BOOKINGS ARRAY:"); + console.log(groupedBookingsArray); + function handleBookingClick(booking) { setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id); } diff --git a/my-app/src/components/booking/BookingsList.module.css b/my-app/src/components/booking/BookingsList.module.css index c9eefa0..e7b3337 100644 --- a/my-app/src/components/booking/BookingsList.module.css +++ b/my-app/src/components/booking/BookingsList.module.css @@ -73,6 +73,9 @@ flex: 1; min-width: 250px; max-width: 500px; + display: flex; + flex-direction: column; + gap: 0.5rem; } .dateGroup:last-child { -- 2.39.5 From 2e0b15ab04cd0ec029e0854ac793b94adef804b1 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:22:52 +0200 Subject: [PATCH 12/20] whole lotta booking card changes (mostly refactor)) --- my-app/src/AppRoutes.jsx | 20 +- my-app/src/components/booking/BookingCard.jsx | 480 +++++------------- .../components/booking/BookingCard.module.css | 217 +++++++- .../components/booking/BookingCardHeader.jsx | 42 ++ .../components/booking/BookingCardModal.jsx | 60 +++ .../booking/BookingCardModal.module.css | 127 +++++ .../booking/BookingCardRefactored.jsx | 160 ++++++ .../components/booking/BookingCardTabs.jsx | 42 ++ .../components/booking/BookingFormContent.jsx | 81 +++ .../booking/BookingOptionsModal.jsx | 73 +++ .../booking/BookingOptionsModal.module.css | 144 ++++++ .../src/components/booking/BookingsList.jsx | 28 +- .../components/booking/ConfirmationDialog.jsx | 47 ++ .../booking/ParticipantBookingContent.jsx | 58 +++ .../booking/ParticipantsDisplay.jsx | 27 + .../components/booking/RoomInfoContent.jsx | 52 ++ .../components/common/NotificationBanner.jsx | 10 + .../common/NotificationBanner.module.css | 23 + my-app/src/components/ui/TimeCard.module.css | 1 + .../ui/TimeCardContainer.module.css | 4 +- my-app/src/context/SettingsContext.jsx | 3 + my-app/src/hooks/useBookingActions.js | 142 ++++++ my-app/src/hooks/useBookingCardState.js | 41 ++ my-app/src/hooks/useResponsiveMode.js | 48 ++ my-app/src/pages/BookingSettings.jsx | 32 ++ my-app/src/pages/RoomBooking.jsx | 8 +- my-app/src/utils/bookingUtils.js | 52 +- 27 files changed, 1667 insertions(+), 355 deletions(-) create mode 100644 my-app/src/components/booking/BookingCardHeader.jsx create mode 100644 my-app/src/components/booking/BookingCardModal.jsx create mode 100644 my-app/src/components/booking/BookingCardModal.module.css create mode 100644 my-app/src/components/booking/BookingCardRefactored.jsx create mode 100644 my-app/src/components/booking/BookingCardTabs.jsx create mode 100644 my-app/src/components/booking/BookingFormContent.jsx create mode 100644 my-app/src/components/booking/BookingOptionsModal.jsx create mode 100644 my-app/src/components/booking/BookingOptionsModal.module.css create mode 100644 my-app/src/components/booking/ConfirmationDialog.jsx create mode 100644 my-app/src/components/booking/ParticipantBookingContent.jsx create mode 100644 my-app/src/components/booking/ParticipantsDisplay.jsx create mode 100644 my-app/src/components/booking/RoomInfoContent.jsx create mode 100644 my-app/src/hooks/useBookingActions.js create mode 100644 my-app/src/hooks/useBookingCardState.js create mode 100644 my-app/src/hooks/useResponsiveMode.js diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 8bc5511..c6b0d8f 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -23,6 +23,10 @@ const AppRoutes = () => { const [lastCreatedBooking, setLastCreatedBooking] = useState(null); const [showDeleteBanner, setShowDeleteBanner] = useState(false); const [lastDeletedBooking, setLastDeletedBooking] = useState(null); + const [showLeaveBanner, setShowLeaveBanner] = useState(false); + const [lastLeftBooking, setLastLeftBooking] = useState(null); + const [showUpdateBanner, setShowUpdateBanner] = useState(false); + const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null); const [bookings, setBookings] = useState([ { id: 1, @@ -113,12 +117,20 @@ const AppRoutes = () => { setBookings(bookings.map(booking => booking.id === updatedBooking.id ? updatedBooking : booking )); + setLastUpdatedBooking(updatedBooking); + setShowUpdateBanner(true); } - function deleteBooking(bookingToDelete) { + function deleteBooking(bookingToDelete, actionType = 'delete') { setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); - setLastDeletedBooking(bookingToDelete); - setShowDeleteBanner(true); + + if (actionType === 'leave') { + setLastLeftBooking(bookingToDelete); + setShowLeaveBanner(true); + } else { + setLastDeletedBooking(bookingToDelete); + setShowDeleteBanner(true); + } } useEffect(() => { @@ -141,7 +153,7 @@ const AppRoutes = () => { } /> }> - setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} /> + setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} showLeaveBanner={showLeaveBanner} lastLeftBooking={lastLeftBooking} onDismissLeaveBanner={() => setShowLeaveBanner(false)} showUpdateBanner={showUpdateBanner} lastUpdatedBooking={lastUpdatedBooking} onDismissUpdateBanner={() => setShowUpdateBanner(false)} />} /> } /> } /> } /> diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index edd2a4d..d2c12a6 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -1,361 +1,163 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Button } from 'react-aria-components'; -import styles from './BookingCard.module.css'; -import { convertDateObjectToString } from '../../helpers'; -import Dropdown from '../ui/Dropdown'; -import { BookingTitleField } from '../forms/BookingTitleField'; -import { ParticipantsSelector } from '../forms/ParticipantsSelector'; +import React, { useState } from 'react'; import { BookingProvider } from '../../context/BookingContext'; -import { Label } from '../ui/Label'; -import { PEOPLE, USER } from '../../constants/bookingConstants'; -function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) { - // Check if this is a participant booking (user was added by someone else) +import { useBookingCardState } from '../../hooks/useBookingCardState'; +import { useBookingActions } from '../../hooks/useBookingActions'; +import { useResponsiveMode } from '../../hooks/useResponsiveMode'; + +import { BookingCardHeader } from './BookingCardHeader'; +import { BookingCardTabs } from './BookingCardTabs'; +import { RoomInfoContent } from './RoomInfoContent'; +import { BookingFormContent } from './BookingFormContent'; +import { ParticipantBookingContent } from './ParticipantBookingContent'; +import { BookingCardModal } from './BookingCardModal'; + +import styles from './BookingCard.module.css'; + +function BookingCard({ + booking, + onClick, + isExpanded, + onBookingUpdate, + onBookingDelete, + editMode = 'inline', + isOptionsExpanded, + onOptionsToggle +}) { + const [isModalOpen, setIsModalOpen] = useState(false); const isParticipantBooking = booking.isParticipantBooking === true; - const [selectedLength, setSelectedLength] = useState(null); - const [calculatedEndTime, setCalculatedEndTime] = useState(null); - const [editedTitle, setEditedTitle] = useState(''); - const [editedParticipants, setEditedParticipants] = useState([]); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - // Calculate current booking length and available hours - const currentLength = booking.endTime - booking.startTime; - const maxAvailableTime = 16; // Max booking slots - const hoursAvailable = Math.min(maxAvailableTime - booking.startTime, 8); - - // Initialize state when card expands - useEffect(() => { - if (isExpanded) { - setSelectedLength(currentLength); - setCalculatedEndTime(booking.endTime); - setEditedTitle(booking.title); - setEditedParticipants(booking.participants || []); - } - }, [isExpanded, booking, currentLength]); + // Custom hooks + const bookingState = useBookingCardState(booking, isExpanded, isModalOpen); + const { effectiveEditMode } = useResponsiveMode(editMode, isExpanded, isModalOpen, onClick, setIsModalOpen); + const actions = useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen); // Create a local booking context for the components const localBookingContext = { - title: editedTitle, - setTitle: setEditedTitle, - participants: editedParticipants, - handleParticipantChange: (participantId) => { - const participant = PEOPLE.find(p => p.id === participantId); - if (participant && !editedParticipants.find(p => p.id === participantId)) { - setEditedParticipants(participants => [...participants, participant]); - } - }, - handleRemoveParticipant: (participantToRemove) => { - setEditedParticipants(participants => - participants.filter(p => p.id !== participantToRemove.id) - ); - } + title: bookingState.editedTitle, + setTitle: bookingState.setEditedTitle, + participants: bookingState.editedParticipants, + handleParticipantChange: actions.handleParticipantChange, + handleRemoveParticipant: actions.handleRemoveParticipant }; - const bookingLengths = []; - for (let i = 1; i <= hoursAvailable; i++) { - const endTimeIndex = booking.startTime + i; - const endTime = getTimeFromIndex(endTimeIndex); - const durationLabel = i === 1 ? "30 min" : - i === 2 ? "1 h" : - i === 3 ? "1.5 h" : - i === 4 ? "2 h" : - i === 5 ? "2.5 h" : - i === 6 ? "3 h" : - i === 7 ? "3.5 h" : - i === 8 ? "4 h" : `${i * 0.5} h`; - - bookingLengths.push({ - value: endTimeIndex, - label: `${endTime} · ${durationLabel}` - }); - } - - // No disabled options needed since we only generate available options - const disabledOptions = {}; - - function handleLengthChange(event) { - const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value); - setCalculatedEndTime(endTimeValue); - - if (endTimeValue !== null) { - const newLength = endTimeValue - booking.startTime; - setSelectedLength(newLength); - } else { - setSelectedLength(currentLength); - setCalculatedEndTime(booking.endTime); - } - } - - // Check if any changes have been made - const hasChanges = () => { - const titleChanged = editedTitle !== booking.title; - const participantsChanged = JSON.stringify(editedParticipants) !== JSON.stringify(booking.participants || []); - const endTimeChanged = calculatedEndTime !== booking.endTime; - return titleChanged || participantsChanged || endTimeChanged; - }; - - function handleSave() { - if (hasChanges() && onBookingUpdate) { - const updatedBooking = { - ...booking, - title: editedTitle, - participants: editedParticipants, - endTime: calculatedEndTime - }; - onBookingUpdate(updatedBooking); - } - onClick(); // Close the expanded view - } - - function handleCancel() { - setSelectedLength(currentLength); - setCalculatedEndTime(booking.endTime); - setEditedTitle(booking.title); - setEditedParticipants(booking.participants || []); - onClick(); // Close the expanded view - } - - function handleDelete() { - setShowDeleteConfirm(true); - } - - function confirmDelete() { - if (onBookingDelete) { - onBookingDelete(booking); - } - setShowDeleteConfirm(false); - } - - function cancelDelete() { - setShowDeleteConfirm(false); - } - - function handleRemoveSelf() { - // For participant bookings, remove the current user from participants - // This effectively "leaves" the booking - if (isParticipantBooking && onBookingDelete) { - onBookingDelete(booking); - } - } - - - function getTimeFromIndex(timeIndex) { - const totalHalfHoursFromStart = timeIndex; - const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - return `${hours}:${minutes === 0 ? '00' : '30'}`; - } - - - function formatParticipants(participants) { - if (!participants || participants.length === 0) return null; - - const formatName = (participant, index) => { - // Bold the booker's name (creator in participant bookings, or current user in regular bookings) - const isBooker = isParticipantBooking - ? (booking.createdBy && participant.id === booking.createdBy.id) - : (participant.id === USER.id); + // Render the expanded content + const renderExpandedContent = (isModal = false) => ( + + {!isModal && ( + actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + setActiveView={bookingState.setActiveView} + /> + )} - const firstName = participant.name.split(' ')[0]; - - /*if (isBooker) { - return {firstName}; - }*/ - return {firstName}; - }; - - if (participants.length === 1) { - return formatName(participants[0], 0); - } else if (participants.length === 2) { - return ( - <> - {formatName(participants[0], 0)} and {formatName(participants[1], 1)} - - ); - } else { - const remaining = participants.length - 2; - return ( - <> - {formatName(participants[0], 0)}, {formatName(participants[1], 1)} and {remaining} more - - ); - } - } + {bookingState.activeView === 'lokalinfo' ? ( + { + bookingState.setActiveView('hantera'); + onClick(); + }} + /> + ) : bookingState.activeView === 'hantera' ? ( + isParticipantBooking ? ( + + ) : ( + + ) + ) : null} + + ); return ( -
-
-
-
-
-

{booking.title}

-
-
- {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} -
- -
-
- {booking.participants && booking.participants.length > 0 && ( -
{formatParticipants(booking.participants)}
- )} -
- {booking.room} -
-
-
- {isExpanded && ( -
- - {isParticipantBooking ? ( - // Participant booking view - read-only with remove self option - <> -
-
- -

{booking.createdBy?.name}

-
-
- -

- {booking.participants - ?.filter(p => p.id !== booking.createdBy?.id) - ?.map(p => p.name) - ?.join(', ')} -

-
-
- - {!showDeleteConfirm ? ( -
- - -
- ) : ( -
-
- ⚠️ -

Är du säker på att du vill lämna denna bokning?

-

- Du kommer inte längre att vara med på "{booking.title}" den {booking.date.day}/{booking.date.month} -

-
-
- - -
-
- )} - - ) : ( - // Regular booking view - editable - <> -
- -
- -
- -
- -
- - -
- -
- - {!showDeleteConfirm ? ( -
- - - -
- ) : ( -
-
- ⚠️ -

Är du säker på att du vill radera denna bokning?

-

- "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} -

-
-
- - -
-
- )} - - )} -
-
- )} + <> +
+
+ -
+ {isExpanded && effectiveEditMode === 'inline' && !bookingState.isRoomInfoModalOpen && ( +
+ {renderExpandedContent()} +
+ )} + + {isOptionsExpanded && !isExpanded && ( +
+ actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + setActiveView={bookingState.setActiveView} + /> +
+ )} +
+ {isParticipantBooking && booking.createdBy && !isExpanded && (
Tillagd av {booking.createdBy.name}
)} +
- -
+ {/* Room information modal */} + { + bookingState.setIsRoomInfoModalOpen(false); + bookingState.setActiveView('closed'); + }} + title="Lokalinformation" + booking={booking} + calculatedEndTime={bookingState.calculatedEndTime} + > + + + + {/* Full edit modal - shown after selecting "Hantera bokning" */} + { + setIsModalOpen(false); + bookingState.setActiveView('closed'); + }} + title={isParticipantBooking ? "Visa bokning" : "Redigera bokning"} + booking={booking} + calculatedEndTime={bookingState.calculatedEndTime} + > + {renderExpandedContent(true)} + + ); } diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index 579cabe..b226129 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -41,6 +41,10 @@ width: 100%; } +.cardWrapper:not(.expanded) .header { + cursor: pointer; +} + .topSection { display: flex; flex-direction: row; @@ -231,11 +235,11 @@ .cancelButton { flex: 2; - background-color: white; + background-color: var(--bg-primary); height: 3rem; - color: #374151; + color: var(--text-primary); font-weight: 600; - border: 2px solid #d1d5db; + border: 1px solid var(--border-light); border-radius: 0.5rem; transition: all 0.2s ease; cursor: pointer; @@ -243,12 +247,12 @@ } .cancelButton:hover { - background-color: #f9fafb; - border-color: #9ca3af; + background-color: var(--bg-secondary); + border-color: var(--border-medium); } .cancelButton:active { - background-color: #e5e7eb; + background-color: var(--bg-tertiary); transform: translateY(1px); } @@ -528,4 +532,205 @@ .cancelDeleteButton[data-focused] { outline: 2px solid #2563EB; outline-offset: -1px; +} + +/* Options expanded card styles */ +.optionsExpanded { + border-color: var(--color-primary); + box-shadow: var(--shadow-xl); +} + +.optionsExpanded .card { + outline: 2px solid #3b82f6; + outline-offset: -1px; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); +} + +.optionsExpanded:hover { + transform: none; +} + +.optionsExpanded .header { + cursor: default; +} + +/* Inline options accordion styles */ +.optionsContent { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-light); + animation: slideDown 0.2s ease-out; +} + +@keyframes slideDown { + from { + max-height: 0; + opacity: 0; + } + to { + max-height: 200px; + opacity: 1; + } +} + +.optionButtons { + display: flex; + flex-direction: row; + gap: 0.75rem; +} + +.optionButton { + background: var(--bg-secondary); + border: 1px solid var(--border-light); + color: var(--text-primary); + padding: 0.75rem 1rem; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + width: 100%; + box-sizing: border-box; + border-radius: var(--border-radius-sm); +} + +.optionButton:hover { + border-color: var(--color-primary); + background-color: var(--bg-secondary); +} + +.optionButton:focus { + outline: none; +} + +.optionButton:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Tab buttons for expanded view */ +.tabButtons { + display: flex; + flex-direction: row; + gap: 0.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border-light); + padding-bottom: 1rem; +} + +.tabButtonsNoBorder { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.tabButton { + background: var(--bg-secondary); + border: 1px solid var(--border-light); + color: var(--text-primary); + padding: 0.75rem 1rem; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + border-radius: var(--border-radius-sm); + flex: 1; +} + +.tabButton:hover { + border-color: var(--color-primary); + background-color: var(--bg-primary); +} + +.tabButton:focus { + outline: none; +} + +.tabButton:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +.activeTab { + background: #6b7280 !important; + color: white !important; + border-color: #4b5563 !important; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.activeTab:hover { + background: #6b7280 !important; + color: white !important; + border-color: #4b5563 !important; +} + +/* Room information styles */ +.roomInfoContent { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.roomImageContainer { + width: 100%; + overflow: hidden; + background-color: var(--bg-secondary); +} + +.roomImage { + width: 100%; + height: 200px; + object-fit: cover; + background-color: #f3f4f6; +} + +.roomDetails { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.roomTitle { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); +} + +.roomInfoGrid { + display: grid; + gap: 0.75rem; +} + +.roomInfoItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-light); +} + +.roomInfoItem:last-child { + border-bottom: none; +} + +.roomInfoLabel { + font-weight: 500; + color: var(--text-secondary); + flex-shrink: 0; +} + +.roomInfoValue { + color: var(--text-primary); + text-align: right; + margin-left: 1rem; +} + +.roomInfoActions { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--border-light); + display: flex; + justify-content: center; } \ No newline at end of file diff --git a/my-app/src/components/booking/BookingCardHeader.jsx b/my-app/src/components/booking/BookingCardHeader.jsx new file mode 100644 index 0000000..a0728cf --- /dev/null +++ b/my-app/src/components/booking/BookingCardHeader.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import styles from './BookingCard.module.css'; +import { getTimeFromIndex } from '../../utils/bookingUtils'; +import { ParticipantsDisplay } from './ParticipantsDisplay'; + +export function BookingCardHeader({ + booking, + calculatedEndTime, + isExpanded, + activeView, + onOptionsToggle, + onClick +}) { + const handleClick = !isExpanded ? onOptionsToggle : (activeView === 'closed' ? onClick : undefined); + + return ( +
+
+
+

{booking.title}

+
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} +
+
+
+ {booking.participants && booking.participants.length > 0 && ( +
+ +
+ )} +
+ {booking.room} +
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingCardModal.jsx b/my-app/src/components/booking/BookingCardModal.jsx new file mode 100644 index 0000000..cc7a627 --- /dev/null +++ b/my-app/src/components/booking/BookingCardModal.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { convertDateObjectToString } from '../../helpers'; +import styles from './BookingCardModal.module.css'; + +export function BookingCardModal({ + isOpen, + onClose, + children, + title = "Redigera bokning", + booking, + calculatedEndTime +}) { + function getTimeFromIndex(timeIndex) { + const totalHalfHoursFromStart = timeIndex; + const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return `${hours}:${minutes === 0 ? '00' : '30'}`; + } + return ( + !open && onClose && onClose()} + className={styles.modal} + > + +
+
+ {booking.title} + {booking && ( +
+
+ {convertDateObjectToString(booking.date)} + + {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)} + + {booking.room} +
+
+ )} +
+ +
+
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingCardModal.module.css b/my-app/src/components/booking/BookingCardModal.module.css new file mode 100644 index 0000000..e839d1f --- /dev/null +++ b/my-app/src/components/booking/BookingCardModal.module.css @@ -0,0 +1,127 @@ +.modal { + --overlay-background: transparent; + padding: 0; + z-index: 1000; + position: relative; +} + +.dialog { + background: var(--bg-primary); + border-radius: 0.75rem; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + max-width: 500px; + width: 100%; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1.5rem 1.5rem 0; + border-bottom: 1px solid var(--border-light); + padding-bottom: 1rem; +} + +.titleSection { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); +} + +.bookingInfo { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.bookingTitle { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); +} + +.bookingDetails { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.date { + font-size: 0.9rem; + color: var(--text-secondary); + font-weight: 500; +} + +.time { + font-size: 0.9rem; + color: var(--text-secondary); + font-weight: 500; +} + +.room { + background-color: var(--su-blue); + color: white; + padding: 0.2rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.85rem; + font-weight: 500; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + color: var(--text-secondary); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.375rem; + line-height: 1; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; +} + +.closeButton:hover { + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +.content { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +@media (max-width: 768px) { + .modal { + padding: 0.5rem; + } + + .dialog { + max-height: 95vh; + } + + .header { + padding: 1rem 1rem 0; + padding-bottom: 0.75rem; + } + + .content { + padding: 1rem; + } +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingCardRefactored.jsx b/my-app/src/components/booking/BookingCardRefactored.jsx new file mode 100644 index 0000000..88e3c97 --- /dev/null +++ b/my-app/src/components/booking/BookingCardRefactored.jsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import { BookingProvider } from '../../context/BookingContext'; +import { PEOPLE } from '../../constants/bookingConstants'; + +import { useBookingCardState } from '../../hooks/useBookingCardState'; +import { useBookingActions } from '../../hooks/useBookingActions'; +import { useResponsiveMode } from '../../hooks/useResponsiveMode'; + +import { BookingCardHeader } from './BookingCardHeader'; +import { BookingCardTabs } from './BookingCardTabs'; +import { RoomInfoContent } from './RoomInfoContent'; +import { BookingFormContent } from './BookingFormContent'; +import { ParticipantBookingContent } from './ParticipantBookingContent'; +import { BookingCardModal } from './BookingCardModal'; + +import styles from './BookingCard.module.css'; + +function BookingCard({ + booking, + onClick, + isExpanded, + onBookingUpdate, + onBookingDelete, + editMode = 'inline', + isOptionsExpanded, + onOptionsToggle +}) { + const [isModalOpen, setIsModalOpen] = useState(false); + const isParticipantBooking = booking.isParticipantBooking === true; + + // Custom hooks + const bookingState = useBookingCardState(booking, isExpanded, isModalOpen); + const { effectiveEditMode } = useResponsiveMode(editMode, isExpanded, isModalOpen, onClick, setIsModalOpen); + const actions = useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen); + + // Create a local booking context for the components + const localBookingContext = { + title: bookingState.editedTitle, + setTitle: bookingState.setEditedTitle, + participants: bookingState.editedParticipants, + handleParticipantChange: actions.handleParticipantChange, + handleRemoveParticipant: actions.handleRemoveParticipant + }; + + // Render the expanded content + const renderExpandedContent = () => ( + + actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + setActiveView={bookingState.setActiveView} + /> + + {bookingState.activeView === 'lokalinfo' ? ( + { + bookingState.setActiveView('hantera'); + onClick(); + }} + /> + ) : bookingState.activeView === 'hantera' ? ( + isParticipantBooking ? ( + + ) : ( + + ) + ) : null} + + ); + + return ( + <> +
+
+ + + {isExpanded && effectiveEditMode === 'inline' && !bookingState.isRoomInfoModalOpen && ( +
+ {renderExpandedContent()} +
+ )} + + {isOptionsExpanded && !isExpanded && ( +
+ actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)} + setActiveView={bookingState.setActiveView} + /> +
+ )} +
+ + {isParticipantBooking && booking.createdBy && !isExpanded && ( +
+ Tillagd av {booking.createdBy.name} +
+ )} +
+ + {/* Room information modal */} + { + bookingState.setIsRoomInfoModalOpen(false); + bookingState.setActiveView('hantera'); + }} + title="Lokalinformation" + booking={booking} + calculatedEndTime={bookingState.calculatedEndTime} + > + + + + {/* Full edit modal - shown after selecting "Hantera bokning" */} + setIsModalOpen(false)} + title={isParticipantBooking ? "Visa bokning" : "Redigera bokning"} + booking={booking} + calculatedEndTime={bookingState.calculatedEndTime} + > + {renderExpandedContent()} + + + ); +} + +export default BookingCard; \ No newline at end of file diff --git a/my-app/src/components/booking/BookingCardTabs.jsx b/my-app/src/components/booking/BookingCardTabs.jsx new file mode 100644 index 0000000..07c086c --- /dev/null +++ b/my-app/src/components/booking/BookingCardTabs.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Button } from 'react-aria-components'; +import styles from './BookingCard.module.css'; + +export function BookingCardTabs({ + activeView, + isInExpandedView = false, + onRoomInfo, + onManageBooking, + setActiveView +}) { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingFormContent.jsx b/my-app/src/components/booking/BookingFormContent.jsx new file mode 100644 index 0000000..ff343c2 --- /dev/null +++ b/my-app/src/components/booking/BookingFormContent.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Button } from 'react-aria-components'; +import styles from './BookingCard.module.css'; +import Dropdown from '../ui/Dropdown'; +import { BookingTitleField } from '../forms/BookingTitleField'; +import { ParticipantsSelector } from '../forms/ParticipantsSelector'; +import { Label } from '../ui/Label'; +import { ConfirmationDialog } from './ConfirmationDialog'; +import { createBookingLengthOptions } from '../../utils/bookingUtils'; + +export function BookingFormContent({ + booking, + calculatedEndTime, + showDeleteConfirm, + hasChanges, + onLengthChange, + onSave, + onCancel, + onDelete, + onConfirmDelete, + onCancelDelete +}) { + const bookingLengths = createBookingLengthOptions(booking); + const disabledOptions = {}; + + return ( + <> +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + {!showDeleteConfirm ? ( +
+ + + +
+ ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingOptionsModal.jsx b/my-app/src/components/booking/BookingOptionsModal.jsx new file mode 100644 index 0000000..6ae8208 --- /dev/null +++ b/my-app/src/components/booking/BookingOptionsModal.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { convertDateObjectToString } from '../../helpers'; +import styles from './BookingOptionsModal.module.css'; + +export function BookingOptionsModal({ + isOpen, + onClose, + booking, + onRoomInfo, + onManageBooking +}) { + function getTimeFromIndex(timeIndex) { + const totalHalfHoursFromStart = timeIndex; + const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return `${hours}:${minutes === 0 ? '00' : '30'}`; + } + + return ( + !open && onClose && onClose()} + className={styles.modal} + > + +
+
+ {booking?.title} + {booking && ( +
+
+ {convertDateObjectToString(booking.date)} + + {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(booking.endTime)} + + {booking.room} +
+
+ )} +
+ +
+
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingOptionsModal.module.css b/my-app/src/components/booking/BookingOptionsModal.module.css new file mode 100644 index 0000000..e15f9c8 --- /dev/null +++ b/my-app/src/components/booking/BookingOptionsModal.module.css @@ -0,0 +1,144 @@ +.modal { + --overlay-background: transparent; + padding: 0; + z-index: 1000; + position: relative; +} + +.dialog { + background: var(--bg-primary); + border-radius: 0.75rem; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + max-width: 400px; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1.5rem 1.5rem 0; + border-bottom: 1px solid var(--border-light); + padding-bottom: 1rem; +} + +.titleSection { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); +} + +.bookingInfo { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.bookingDetails { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.date { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +.time { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +.room { + background-color: var(--su-blue); + color: white; + padding: 0.15rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + color: var(--text-secondary); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.375rem; + line-height: 1; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +.closeButton:hover { + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +.content { + padding: 1.5rem; +} + +.buttonGroup { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.optionButton { + background: var(--bg-primary); + border: 2px solid var(--border-light); + color: var(--text-primary); + padding: 1rem 1.5rem; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; +} + +.optionButton:hover { + border-color: var(--color-primary); + background-color: var(--bg-secondary); +} + +.optionButton:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +@media (max-width: 768px) { + .modal { + padding: 0.5rem; + } + + .header { + padding: 1rem 1rem 0; + padding-bottom: 0.75rem; + } + + .content { + padding: 1rem; + } +} \ No newline at end of file diff --git a/my-app/src/components/booking/BookingsList.jsx b/my-app/src/components/booking/BookingsList.jsx index 690a9a6..347ef71 100644 --- a/my-app/src/components/booking/BookingsList.jsx +++ b/my-app/src/components/booking/BookingsList.jsx @@ -3,9 +3,12 @@ import { CalendarDate } from '@internationalized/date'; import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; import NotificationBanner from '../common/NotificationBanner'; +import { useSettingsContext } from '../../context/SettingsContext'; -function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) { +function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showLeaveBanner, lastLeftBooking, onDismissLeaveBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner, showUpdateBanner, lastUpdatedBooking, onDismissUpdateBanner }) { + const { settings } = useSettingsContext(); const [expandedBookingId, setExpandedBookingId] = useState(null); + const [optionsExpandedBookingId, setOptionsExpandedBookingId] = useState(null); // Sort bookings by date (earliest first) const sortedBookings = [...bookings].sort((a, b) => { @@ -35,6 +38,10 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id); } + function handleOptionsToggle(booking) { + setOptionsExpandedBookingId(optionsExpandedBookingId === booking.id ? null : booking.id); + } + function formatDateHeader(dateObj) { const days = ['SÖNDAG', 'MÅNDAG', 'TISDAG', 'ONSDAG', 'TORSDAG', 'FREDAG', 'LÖRDAG']; const months = ['JANUARI', 'FEBRUARI', 'MARS', 'APRIL', 'MAJ', 'JUNI', 'JULI', 'AUGUSTI', 'SEPTEMBER', 'OKTOBER', 'NOVEMBER', 'DECEMBER']; @@ -64,6 +71,22 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD showCloseButton={true} /> )} + {showLeaveBanner && ( + + )} + {showUpdateBanner && ( + + )} {showDevelopmentBanner && ( handleOptionsToggle(booking)} /> ))}
diff --git a/my-app/src/components/booking/ConfirmationDialog.jsx b/my-app/src/components/booking/ConfirmationDialog.jsx new file mode 100644 index 0000000..5e96d8b --- /dev/null +++ b/my-app/src/components/booking/ConfirmationDialog.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Button } from 'react-aria-components'; +import styles from './BookingCard.module.css'; +import { getTimeFromIndex } from '../../utils/bookingUtils'; + +export function ConfirmationDialog({ + booking, + isParticipantBooking = false, + onConfirm, + onCancel +}) { + const isLeaveDialog = isParticipantBooking; + + return ( +
+
+ ⚠️ +

+ {isLeaveDialog + ? "Är du säker på att du vill lämna denna bokning?" + : "Är du säker på att du vill radera denna bokning?" + } +

+

+ {isLeaveDialog + ? `Du kommer inte längre att vara med på "${booking.title}" den ${booking.date.day}/${booking.date.month}` + : `"${booking.title}" den ${booking.date.day}/${booking.date.month} kl. ${getTimeFromIndex(booking.startTime)}` + } +

+
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/ParticipantBookingContent.jsx b/my-app/src/components/booking/ParticipantBookingContent.jsx new file mode 100644 index 0000000..bf0c4be --- /dev/null +++ b/my-app/src/components/booking/ParticipantBookingContent.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Button } from 'react-aria-components'; +import styles from './BookingCard.module.css'; +import { Label } from '../ui/Label'; +import { ConfirmationDialog } from './ConfirmationDialog'; + +export function ParticipantBookingContent({ + booking, + showDeleteConfirm, + onRemoveSelf, + onCancel, + onSetShowDeleteConfirm, + onCancelDelete +}) { + return ( + <> +
+
+ +

{booking.createdBy?.name}

+
+
+ +

+ {booking.participants + ?.filter(p => p.id !== booking.createdBy?.id) + ?.map(p => p.name) + ?.join(', ')} +

+
+
+ + {!showDeleteConfirm ? ( +
+ + +
+ ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/ParticipantsDisplay.jsx b/my-app/src/components/booking/ParticipantsDisplay.jsx new file mode 100644 index 0000000..71c94c2 --- /dev/null +++ b/my-app/src/components/booking/ParticipantsDisplay.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +export function ParticipantsDisplay({ participants, isParticipantBooking = false, createdBy = null }) { + if (!participants || participants.length === 0) return null; + + const formatName = (participant, index) => { + const firstName = participant.name.split(' ')[0]; + return {firstName}; + }; + + if (participants.length === 1) { + return formatName(participants[0], 0); + } else if (participants.length === 2) { + return ( + <> + {formatName(participants[0], 0)} and {formatName(participants[1], 1)} + + ); + } else { + const remaining = participants.length - 2; + return ( + <> + {formatName(participants[0], 0)}, {formatName(participants[1], 1)} and {remaining} more + + ); + } +} \ No newline at end of file diff --git a/my-app/src/components/booking/RoomInfoContent.jsx b/my-app/src/components/booking/RoomInfoContent.jsx new file mode 100644 index 0000000..9d3c9b3 --- /dev/null +++ b/my-app/src/components/booking/RoomInfoContent.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Button } from 'react-aria-components'; +import styles from './BookingCard.module.css'; + +export function RoomInfoContent({ booking, showCloseButton = false, onClose }) { + return ( +
+
+ {`${booking.room} { + // Fallback to a default room image if specific room image doesn't exist + e.target.src = '/images/rooms/default-room.jpg'; + }} + /> +
+
+

Rum: {booking.room}

+
+
+ Kategori: + Litet grupprum +
+
+ Kapacitet: + 5 personer +
+
+ Utrustning: + TV, Tangentbord, Whiteboard +
+
+ Övrig info: + En stol trasig +
+
+ {showCloseButton && ( +
+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/common/NotificationBanner.jsx b/my-app/src/components/common/NotificationBanner.jsx index 55b809c..4258ea3 100644 --- a/my-app/src/components/common/NotificationBanner.jsx +++ b/my-app/src/components/common/NotificationBanner.jsx @@ -8,11 +8,21 @@ const BANNER_VARIANTS = { title: 'Bokning bekräftad:', className: 'success' }, + update: { + icon: '✓', + title: 'Bokning uppdaterad:', + className: 'success' + }, delete: { icon: '🗑️', title: 'Bokning raderad:', className: 'delete' }, + leave: { + icon: '👋', + title: 'Du har lämnat bokningen:', + className: 'leave' + }, development: { icon: '🔧', title: 'Visar testdata för utveckling', diff --git a/my-app/src/components/common/NotificationBanner.module.css b/my-app/src/components/common/NotificationBanner.module.css index 8c0a952..ee03d14 100644 --- a/my-app/src/components/common/NotificationBanner.module.css +++ b/my-app/src/components/common/NotificationBanner.module.css @@ -7,6 +7,10 @@ align-items: center; justify-content: space-between; box-shadow: var(--shadow-lg); + position: sticky; + top: 5rem; + z-index: 100; + width: 100%; } .bannerContent { @@ -111,6 +115,25 @@ color: var(--notification-error-details); } +/* Leave variant styles */ +.leave { + background: var(--notification-warning-bg); + border: var(--border-width-thin) solid var(--notification-warning-border); +} + +.leaveIcon { + background: var(--notification-warning-icon-bg); + color: var(--notification-warning-icon-text); +} + +.leaveTitle { + color: var(--notification-warning-title); +} + +.leaveDetails { + color: var(--notification-warning-details); +} + /* Development variant styles */ .development { background: var(--notification-warning-bg); diff --git a/my-app/src/components/ui/TimeCard.module.css b/my-app/src/components/ui/TimeCard.module.css index e529c49..e704566 100644 --- a/my-app/src/components/ui/TimeCard.module.css +++ b/my-app/src/components/ui/TimeCard.module.css @@ -84,6 +84,7 @@ border: 1px solid var(--timecard-unavailable-border); height: 50px; width: 165px; + background: red; } .modalFooter { diff --git a/my-app/src/components/ui/TimeCardContainer.module.css b/my-app/src/components/ui/TimeCardContainer.module.css index 56c82fa..7571799 100644 --- a/my-app/src/components/ui/TimeCardContainer.module.css +++ b/my-app/src/components/ui/TimeCardContainer.module.css @@ -71,8 +71,8 @@ } .pairRow > * { - flex: 0 0 135px; - width: 135px; + /*flex: 0 0 135px;*/ + /*width: 135px;*/ } } diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 9fa5a99..9137c95 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -34,6 +34,7 @@ export const SettingsProvider = ({ children }) => { bookingFormType: 'inline', // 'modal' or 'inline' showFiltersAlways: false, // Show filter dropdowns always or behind toggle button newBookingLayoutVariant: false, // false = stacked, true = side-by-side + bookingCardEditMode: 'inline', // 'inline', 'modal', or 'responsive' // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -118,6 +119,8 @@ export const SettingsProvider = ({ children }) => { showBookingDeleteBanner: false, bookingFormType: 'inline', showFiltersAlways: false, + newBookingLayoutVariant: false, + bookingCardEditMode: 'inline', }); localStorage.removeItem('calendarSettings'); }; diff --git a/my-app/src/hooks/useBookingActions.js b/my-app/src/hooks/useBookingActions.js new file mode 100644 index 0000000..5e0a2e2 --- /dev/null +++ b/my-app/src/hooks/useBookingActions.js @@ -0,0 +1,142 @@ +import { useCallback } from 'react'; +import { PEOPLE } from '../constants/bookingConstants'; +import { hasBookingChanges } from '../utils/bookingUtils'; + +export function useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen) { + const { + calculatedEndTime, + setCalculatedEndTime, + editedTitle, + setEditedTitle, + editedParticipants, + setEditedParticipants, + resetState, + setShowDeleteConfirm, + setActiveView, + setIsRoomInfoModalOpen + } = bookingState; + + const handleLengthChange = useCallback((event) => { + const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value); + setCalculatedEndTime(endTimeValue); + + if (endTimeValue === null) { + setCalculatedEndTime(booking.endTime); + } + }, [booking.endTime, setCalculatedEndTime]); + + const handleParticipantChange = useCallback((participantId) => { + const participant = PEOPLE.find(p => p.id === participantId); + if (participant && !editedParticipants.find(p => p.id === participantId)) { + setEditedParticipants(participants => [...participants, participant]); + } + }, [editedParticipants, setEditedParticipants]); + + const handleRemoveParticipant = useCallback((participantToRemove) => { + setEditedParticipants(participants => + participants.filter(p => p.id !== participantToRemove.id) + ); + }, [setEditedParticipants]); + + const hasChanges = useCallback(() => { + return hasBookingChanges(booking, editedTitle, editedParticipants, calculatedEndTime); + }, [booking, editedTitle, editedParticipants, calculatedEndTime]); + + const handleSave = useCallback(() => { + if (hasChanges() && onBookingUpdate) { + const updatedBooking = { + ...booking, + title: editedTitle, + participants: editedParticipants, + endTime: calculatedEndTime + }; + onBookingUpdate(updatedBooking); + } + + if (effectiveEditMode === 'modal') { + setIsModalOpen(false); + } else { + onClick(); // Close the expanded view + } + }, [hasChanges, onBookingUpdate, booking, editedTitle, editedParticipants, calculatedEndTime, effectiveEditMode, setIsModalOpen, onClick]); + + const handleCancel = useCallback(() => { + resetState(); + + if (effectiveEditMode === 'modal') { + setIsModalOpen(false); + setActiveView('closed'); + } else { + onClick(); // Close the expanded view + } + }, [resetState, effectiveEditMode, setIsModalOpen, onClick, setActiveView]); + + const handleDelete = useCallback(() => { + setShowDeleteConfirm(true); + }, [setShowDeleteConfirm]); + + const confirmDelete = useCallback(() => { + if (onBookingDelete) { + onBookingDelete(booking); + } + setShowDeleteConfirm(false); + }, [onBookingDelete, booking, setShowDeleteConfirm]); + + const cancelDelete = useCallback(() => { + setShowDeleteConfirm(false); + }, [setShowDeleteConfirm]); + + const handleRemoveSelf = useCallback(() => { + if (onBookingDelete) { + onBookingDelete(booking, 'leave'); + } + }, [onBookingDelete, booking]); + + const handleRoomInfo = useCallback((effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded) => { + setActiveView('lokalinfo'); + + if (effectiveEditMode === 'modal') { + // For modal mode, don't close accordion - keep it for when modal closes + setIsRoomInfoModalOpen(true); + } else { + // For inline mode + if (isOptionsExpanded) { + onOptionsToggle(); + } + if (!isExpanded) { + onClick(); + } + } + }, [setActiveView, setIsRoomInfoModalOpen, onClick]); + + const handleManageBooking = useCallback((effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded) => { + setActiveView('hantera'); + + if (effectiveEditMode === 'modal') { + setIsModalOpen(true); + // Don't close the options accordion for modal mode - keep it for when modal closes + } else { + if (isOptionsExpanded) { + onOptionsToggle(); // Close the options accordion only for inline mode + } + if (!isExpanded) { + onClick(); // Open inline expansion + } + } + }, [setActiveView, setIsModalOpen, onClick]); + + return { + handleLengthChange, + handleParticipantChange, + handleRemoveParticipant, + hasChanges, + handleSave, + handleCancel, + handleDelete, + confirmDelete, + cancelDelete, + handleRemoveSelf, + handleRoomInfo, + handleManageBooking + }; +} \ No newline at end of file diff --git a/my-app/src/hooks/useBookingCardState.js b/my-app/src/hooks/useBookingCardState.js new file mode 100644 index 0000000..db68a26 --- /dev/null +++ b/my-app/src/hooks/useBookingCardState.js @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; + +export function useBookingCardState(booking, isExpanded, isModalOpen) { + const [calculatedEndTime, setCalculatedEndTime] = useState(null); + const [editedTitle, setEditedTitle] = useState(''); + const [editedParticipants, setEditedParticipants] = useState([]); + const [activeView, setActiveView] = useState('closed'); // 'hantera', 'lokalinfo', or 'closed' + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isRoomInfoModalOpen, setIsRoomInfoModalOpen] = useState(false); + + // Initialize state when card expands or modal opens + useEffect(() => { + if (isExpanded || isModalOpen) { + setCalculatedEndTime(booking.endTime); + setEditedTitle(booking.title); + setEditedParticipants(booking.participants || []); + } + }, [isExpanded, isModalOpen, booking]); + + const resetState = () => { + setCalculatedEndTime(booking.endTime); + setEditedTitle(booking.title); + setEditedParticipants(booking.participants || []); + }; + + return { + calculatedEndTime, + setCalculatedEndTime, + editedTitle, + setEditedTitle, + editedParticipants, + setEditedParticipants, + activeView, + setActiveView, + showDeleteConfirm, + setShowDeleteConfirm, + isRoomInfoModalOpen, + setIsRoomInfoModalOpen, + resetState + }; +} \ No newline at end of file diff --git a/my-app/src/hooks/useResponsiveMode.js b/my-app/src/hooks/useResponsiveMode.js new file mode 100644 index 0000000..7643257 --- /dev/null +++ b/my-app/src/hooks/useResponsiveMode.js @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react'; + +export function useResponsiveMode(editMode, isExpanded, isModalOpen, onClick, setIsModalOpen) { + const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024); + const [previousWidth, setPreviousWidth] = useState(windowWidth); + + // Handle window resize for responsive mode + useEffect(() => { + const handleResize = () => { + setPreviousWidth(windowWidth); + setWindowWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [windowWidth]); + + // Determine effective edit mode based on settings and screen width + const effectiveEditMode = editMode === 'responsive' + ? (windowWidth > 780 ? 'modal' : 'inline') + : editMode; + + // Handle mode transitions when window is resized + useEffect(() => { + if (editMode === 'responsive') { + const wasInlineMode = previousWidth <= 780; + const isNowModalMode = windowWidth > 780; + const wasModalMode = previousWidth > 780; + const isNowInlineMode = windowWidth <= 780; + + // If card was expanded inline and window becomes wide, switch to modal + if (isExpanded && wasInlineMode && isNowModalMode) { + setIsModalOpen(true); + onClick(); // Close inline expansion + } + // If modal was open and window becomes narrow, switch to inline + else if (isModalOpen && wasModalMode && isNowInlineMode) { + setIsModalOpen(false); + onClick(); // Open inline expansion + } + } + }, [windowWidth, previousWidth, editMode, isExpanded, isModalOpen, onClick, setIsModalOpen]); + + return { + windowWidth, + effectiveEditMode + }; +} \ No newline at end of file diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index ce196e9..d3f5d45 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -221,6 +221,38 @@ export function BookingSettings() { Side-by-Side: Image/header on left, booking times on right (medium screens)
+ +
+ + +
+ Current: + {settings.bookingCardEditMode === 'inline' ? 'Inline Expansion' : + settings.bookingCardEditMode === 'modal' ? 'Modal Popup' : + settings.bookingCardEditMode === 'responsive' ? 'Responsive (Modal on Desktop)' : + 'Unknown'} + +
+
+ Inline Expansion: Card expands directly in the list for editing
+ Modal Popup: Always opens in a centered modal dialog
+ Responsive: Inline on mobile (≤780px), modal on desktop (>780px) +
+
diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index b86ccd4..ceb0380 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -8,7 +8,7 @@ import { USER } from '../constants/bookingConstants'; import PageHeader from '../components/layout/PageHeader'; import PageContainer from '../components/layout/PageContainer'; -export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) { +export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showLeaveBanner, lastLeftBooking, onDismissLeaveBanner, showUpdateBanner, lastUpdatedBooking, onDismissUpdateBanner }) { const { settings } = useSettingsContext(); useEffect(() => { @@ -61,6 +61,12 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={onDismissDeleteBanner} + showLeaveBanner={showLeaveBanner} + lastLeftBooking={lastLeftBooking} + onDismissLeaveBanner={onDismissLeaveBanner} + showUpdateBanner={showUpdateBanner} + lastUpdatedBooking={lastUpdatedBooking} + onDismissUpdateBanner={onDismissUpdateBanner} showDevelopmentBanner={settings.showDevelopmentBanner} showBookingConfirmationBanner={settings.showBookingConfirmationBanner} showBookingDeleteBanner={settings.showBookingDeleteBanner} diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index d8537a2..fec1d7b 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -1,5 +1,6 @@ +import React from 'react'; import { today, getLocalTimeZone } from '@internationalized/date'; -import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY } from '../constants/bookingConstants'; +import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY, USER } from '../constants/bookingConstants'; export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 23) => { return [...Array(numberOfRooms)].map((room, index) => ({ @@ -87,4 +88,51 @@ export const isDateUnavailable = (date, effectiveToday, bookingRangeDays = 14) = date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); -}; \ No newline at end of file +}; + +export function getParticipantNames(participants) { + if (!participants || participants.length === 0) return null; + + const getFirstName = (participant) => participant.name.split(' ')[0]; + + if (participants.length === 1) { + return getFirstName(participants[0]); + } else if (participants.length === 2) { + return `${getFirstName(participants[0])} and ${getFirstName(participants[1])}`; + } else { + const remaining = participants.length - 2; + return `${getFirstName(participants[0])}, ${getFirstName(participants[1])} and ${remaining} more`; + } +} + +export function createBookingLengthOptions(booking, maxAvailableTime = 16) { + const hoursAvailable = Math.min(maxAvailableTime - booking.startTime, 8); + const bookingLengths = []; + + for (let i = 1; i <= hoursAvailable; i++) { + const endTimeIndex = booking.startTime + i; + const endTime = getTimeFromIndex(endTimeIndex); + const durationLabel = i === 1 ? "30 min" : + i === 2 ? "1 h" : + i === 3 ? "1.5 h" : + i === 4 ? "2 h" : + i === 5 ? "2.5 h" : + i === 6 ? "3 h" : + i === 7 ? "3.5 h" : + i === 8 ? "4 h" : `${i * 0.5} h`; + + bookingLengths.push({ + value: endTimeIndex, + label: `${endTime} · ${durationLabel}` + }); + } + + return bookingLengths; +} + +export function hasBookingChanges(originalBooking, editedTitle, editedParticipants, calculatedEndTime) { + const titleChanged = editedTitle !== originalBooking.title; + const participantsChanged = JSON.stringify(editedParticipants) !== JSON.stringify(originalBooking.participants || []); + const endTimeChanged = calculatedEndTime !== originalBooking.endTime; + return titleChanged || participantsChanged || endTimeChanged; +} \ No newline at end of file -- 2.39.5 From 7b9c80f97f9146fa07d3e29ae240b48c6ebbe905 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:25:30 +0200 Subject: [PATCH 13/20] small saving fix --- my-app/src/components/booking/BookingCard.jsx | 2 +- my-app/src/hooks/useBookingActions.js | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/my-app/src/components/booking/BookingCard.jsx b/my-app/src/components/booking/BookingCard.jsx index d2c12a6..e23cd63 100644 --- a/my-app/src/components/booking/BookingCard.jsx +++ b/my-app/src/components/booking/BookingCard.jsx @@ -30,7 +30,7 @@ function BookingCard({ // Custom hooks const bookingState = useBookingCardState(booking, isExpanded, isModalOpen); const { effectiveEditMode } = useResponsiveMode(editMode, isExpanded, isModalOpen, onClick, setIsModalOpen); - const actions = useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen); + const actions = useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen, onOptionsToggle, isOptionsExpanded); // Create a local booking context for the components const localBookingContext = { diff --git a/my-app/src/hooks/useBookingActions.js b/my-app/src/hooks/useBookingActions.js index 5e0a2e2..269b043 100644 --- a/my-app/src/hooks/useBookingActions.js +++ b/my-app/src/hooks/useBookingActions.js @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { PEOPLE } from '../constants/bookingConstants'; import { hasBookingChanges } from '../utils/bookingUtils'; -export function useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen) { +export function useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen, onOptionsToggle, isOptionsExpanded) { const { calculatedEndTime, setCalculatedEndTime, @@ -55,10 +55,15 @@ export function useBookingActions(booking, bookingState, onBookingUpdate, onBook if (effectiveEditMode === 'modal') { setIsModalOpen(false); + setActiveView('closed'); + // Close options accordion if it's expanded + if (isOptionsExpanded) { + onOptionsToggle(); + } } else { onClick(); // Close the expanded view } - }, [hasChanges, onBookingUpdate, booking, editedTitle, editedParticipants, calculatedEndTime, effectiveEditMode, setIsModalOpen, onClick]); + }, [hasChanges, onBookingUpdate, booking, editedTitle, editedParticipants, calculatedEndTime, effectiveEditMode, setIsModalOpen, onClick, setActiveView, onOptionsToggle, isOptionsExpanded]); const handleCancel = useCallback(() => { resetState(); -- 2.39.5 From dab8e94230aaedb89aa60eac3b7fff242c3d3b79 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:39:05 +0200 Subject: [PATCH 14/20] detail fixes --- .../components/booking/BookingCard.module.css | 89 ------------------- .../components/booking/BookingCardTabs.jsx | 22 +++-- .../booking/BookingCardTabs.module.css | 86 ++++++++++++++++++ 3 files changed, 102 insertions(+), 95 deletions(-) create mode 100644 my-app/src/components/booking/BookingCardTabs.module.css diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css index b226129..da96e6e 100644 --- a/my-app/src/components/booking/BookingCard.module.css +++ b/my-app/src/components/booking/BookingCard.module.css @@ -573,97 +573,8 @@ } } -.optionButtons { - display: flex; - flex-direction: row; - gap: 0.75rem; -} -.optionButton { - background: var(--bg-secondary); - border: 1px solid var(--border-light); - color: var(--text-primary); - padding: 0.75rem 1rem; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; - width: 100%; - box-sizing: border-box; - border-radius: var(--border-radius-sm); -} -.optionButton:hover { - border-color: var(--color-primary); - background-color: var(--bg-secondary); -} - -.optionButton:focus { - outline: none; -} - -.optionButton:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 2px; -} - -/* Tab buttons for expanded view */ -.tabButtons { - display: flex; - flex-direction: row; - gap: 0.5rem; - margin-bottom: 1rem; - border-bottom: 1px solid var(--border-light); - padding-bottom: 1rem; -} - -.tabButtonsNoBorder { - display: flex; - flex-direction: row; - gap: 0.5rem; -} - -.tabButton { - background: var(--bg-secondary); - border: 1px solid var(--border-light); - color: var(--text-primary); - padding: 0.75rem 1rem; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; - border-radius: var(--border-radius-sm); - flex: 1; -} - -.tabButton:hover { - border-color: var(--color-primary); - background-color: var(--bg-primary); -} - -.tabButton:focus { - outline: none; -} - -.tabButton:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 2px; -} - -.activeTab { - background: #6b7280 !important; - color: white !important; - border-color: #4b5563 !important; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.activeTab:hover { - background: #6b7280 !important; - color: white !important; - border-color: #4b5563 !important; -} /* Room information styles */ .roomInfoContent { diff --git a/my-app/src/components/booking/BookingCardTabs.jsx b/my-app/src/components/booking/BookingCardTabs.jsx index 07c086c..99282a8 100644 --- a/my-app/src/components/booking/BookingCardTabs.jsx +++ b/my-app/src/components/booking/BookingCardTabs.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from 'react-aria-components'; -import styles from './BookingCard.module.css'; +import styles from './BookingCardTabs.module.css'; export function BookingCardTabs({ activeView, @@ -9,11 +9,18 @@ export function BookingCardTabs({ onManageBooking, setActiveView }) { + const containerClass = isInExpandedView && activeView !== 'closed' + ? styles.tabButtons + : styles.tabButtonsNoBorder; + return ( -
+
- ))} + {booking.participants.map((participant, index) => { + const isCurrentUser = participant.id === getCurrentUser().id; + + return isCurrentUser ? ( + /* Current user appears as non-removable pill */ +
+ {participant.name} +
+ ) : ( + /* Other participants are removable */ + + ); + })}
); -- 2.39.5 From 647b4bf8e0ccc965c48add53c11c511384ad81f1 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:07:52 +0200 Subject: [PATCH 18/20] preparing user tests --- my-app/src/App.jsx | 34 ++++- my-app/src/AppRoutes.jsx | 16 ++- .../InlineModalExtendedBookingForm.jsx | 10 +- my-app/src/components/ui/NamePrompt.jsx | 107 ++++++++++++++ .../src/components/ui/NamePrompt.module.css | 132 ++++++++++++++++++ my-app/src/components/ui/TimeCard.module.css | 9 +- my-app/src/context/SettingsContext.jsx | 18 ++- my-app/src/pages/Profile.jsx | 2 + my-app/src/pages/RoomBooking.jsx | 9 +- 9 files changed, 307 insertions(+), 30 deletions(-) create mode 100644 my-app/src/components/ui/NamePrompt.jsx create mode 100644 my-app/src/components/ui/NamePrompt.module.css diff --git a/my-app/src/App.jsx b/my-app/src/App.jsx index 9b891e3..e25c7d5 100644 --- a/my-app/src/App.jsx +++ b/my-app/src/App.jsx @@ -1,16 +1,40 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import AppRoutes from './AppRoutes'; // move the routing and loading logic here -import { SettingsProvider } from './context/SettingsContext'; +import { SettingsProvider, useSettingsContext } from './context/SettingsContext'; import { ThemeProvider } from './context/ThemeContext'; +import { NamePrompt } from './components/ui/NamePrompt'; + +function AppContent() { + const { shouldShowNamePrompt } = useSettingsContext(); + const [showNamePrompt, setShowNamePrompt] = useState(false); + + useEffect(() => { + setShowNamePrompt(shouldShowNamePrompt()); + }, [shouldShowNamePrompt]); + + const handleCloseNamePrompt = () => { + setShowNamePrompt(false); + }; + + return ( + <> + + + + + + ); +} function App() { return ( - - - + ); diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index c6b0d8f..6cac725 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -15,6 +15,7 @@ import Home from './pages/Home'; import CoursePage from './pages/CoursePage'; import Profile from './pages/Profile'; import RoomSchedules from './pages/RoomSchedules'; +import { useSettingsContext } from './context/SettingsContext'; const AppRoutes = () => { const location = useLocation(); @@ -27,6 +28,11 @@ const AppRoutes = () => { const [lastLeftBooking, setLastLeftBooking] = useState(null); const [showUpdateBanner, setShowUpdateBanner] = useState(false); const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null); + const { getCurrentUser } = useSettingsContext(); + const currentUser = getCurrentUser(); + + // Mock bookings data + // In a real app, this would come from an API or global state const [bookings, setBookings] = useState([ { id: 1, @@ -37,7 +43,7 @@ const AppRoutes = () => { roomCategory: 'green', title: 'Team standup', participants: [ - { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + currentUser, { id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' }, { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, { id: 4, name: 'Elin Rudling', username: 'elru4521', email: 'elin.rudling@dsv.su.se' } @@ -52,7 +58,7 @@ const AppRoutes = () => { roomCategory: 'red', title: 'Project planning workshop', participants: [ - { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + currentUser, { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, { id: 6, name: 'Ellen Britschgi', username: 'elbr5623', email: 'ellen.britschgi@dsv.su.se' }, { id: 7, name: 'Anna Andersson', username: 'anan3457', email: 'anna.andersson@dsv.su.se' }, @@ -70,7 +76,7 @@ const AppRoutes = () => { roomCategory: 'blue', title: '1:1 with supervisor', participants: [ - { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + currentUser, { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' } ] }, @@ -83,7 +89,7 @@ const AppRoutes = () => { roomCategory: 'yellow', title: 'Study group session', participants: [ - { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + currentUser, { id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' }, { id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' } ] @@ -99,7 +105,7 @@ const AppRoutes = () => { createdBy: { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, participants: [ { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, - { id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' }, + currentUser, { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' } ], diff --git a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx index 5407c7c..ab67a05 100644 --- a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx +++ b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx @@ -9,7 +9,6 @@ import { ParticipantsSelector } from '../forms/ParticipantsSelector'; import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; import { generateId } from '../../utils/bookingUtils'; -import { USER } from '../../constants/bookingConstants'; import styles from './InlineModalBookingForm.module.css'; import extendedStyles from './InlineModalExtendedBookingForm.module.css'; @@ -41,6 +40,7 @@ export function InlineModalExtendedBookingForm({ const navigate = useNavigate(); const booking = useBookingContext(); const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext(); + const currentUser = getCurrentUser(); // Initialize with pre-selected end time if available, or auto-select if only 30 min available const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength : @@ -161,9 +161,9 @@ export function InlineModalExtendedBookingForm({ const roomToBook = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom; // Include the current user as a participant if not already added - const allParticipants = booking.participants.find(p => p.id === USER.id) + const allParticipants = booking.participants.find(p => p.id === currentUser.id) ? booking.participants - : [USER, ...booking.participants]; + : [currentUser, ...booking.participants]; const finalTitle = booking.title !== "" ? booking.title : getDefaultBookingTitle(); @@ -204,9 +204,9 @@ export function InlineModalExtendedBookingForm({

{(() => { // Include the current user as a participant if not already added - const allParticipants = booking.participants.find(p => p.id === USER.id) + const allParticipants = booking.participants.find(p => p.id === currentUser.id) ? booking.participants - : [USER, ...booking.participants]; + : [currentUser, ...booking.participants]; const startTime = getTimeFromIndex(booking.selectedStartIndex); const endTime = getTimeFromIndex(booking.selectedEndIndex); diff --git a/my-app/src/components/ui/NamePrompt.jsx b/my-app/src/components/ui/NamePrompt.jsx new file mode 100644 index 0000000..46019a5 --- /dev/null +++ b/my-app/src/components/ui/NamePrompt.jsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { useSettingsContext } from '../../context/SettingsContext'; +import styles from './NamePrompt.module.css'; + +export function NamePrompt({ isOpen, onClose }) { + const { updateSettings } = useSettingsContext(); + const [name, setName] = useState(''); + const [error, setError] = useState(''); + + // Helper function to generate username from name + const generateUsername = (fullName) => { + const nameParts = fullName.trim().toLowerCase().split(' '); + if (nameParts.length >= 2) { + const firstName = nameParts[0]; + const lastName = nameParts[nameParts.length - 1]; + // Take first 2 chars of first name + first 2 chars of last name + random 4 digits + const randomDigits = Math.floor(1000 + Math.random() * 9000); + return `${firstName.substring(0, 2)}${lastName.substring(0, 2)}${randomDigits}`; + } else { + // Fallback for single name + const randomDigits = Math.floor(1000 + Math.random() * 9000); + return `${nameParts[0].substring(0, 4)}${randomDigits}`; + } + }; + + // Helper function to generate email from username + const generateEmail = (username) => { + return `${username}@student.su.se`; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!name.trim()) { + setError('Vänligen ange ditt namn'); + return; + } + + const trimmedName = name.trim(); + const generatedUsername = generateUsername(trimmedName); + const generatedEmail = generateEmail(generatedUsername); + + // Update settings with name, username, and email + updateSettings({ + currentUserName: trimmedName, + currentUserUsername: generatedUsername, + currentUserEmail: generatedEmail + }); + localStorage.setItem('hasSeenNamePrompt', 'true'); + onClose(); + + // Refresh the page to ensure all components get the updated user data + window.location.reload(); + }; + + const handleInputChange = (e) => { + setName(e.target.value); + if (error) setError(''); + }; + + return ( + + +
+ + Välkommen! + +

+ För att komma igång behöver vi veta vad du heter. Ditt namn kommer att visas i bokningar och deltagarlistan. +

+ +
+
+ + + {error && {error}} +
+ +
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/ui/NamePrompt.module.css b/my-app/src/components/ui/NamePrompt.module.css new file mode 100644 index 0000000..2778ac8 --- /dev/null +++ b/my-app/src/components/ui/NamePrompt.module.css @@ -0,0 +1,132 @@ +.modal { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + inset: 0; + z-index: 100; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.dialog { + background: white; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + max-width: 480px; + width: 90vw; + padding: 0; + border: none; + overflow: hidden; +} + +.content { + padding: 32px; +} + +.title { + font-size: 24px; + font-weight: 600; + margin: 0 0 16px 0; + color: #111827; + text-align: center; +} + +.description { + font-size: 16px; + line-height: 1.5; + color: #6b7280; + margin: 0 0 24px 0; + text-align: center; +} + +.form { + display: flex; + flex-direction: column; + gap: 24px; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: 14px; + font-weight: 500; + color: #374151; +} + +.input { + padding: 12px 16px; + border: 2px solid #d1d5db; + border-radius: 8px; + font-size: 16px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.inputError { + border-color: #ef4444; +} + +.inputError:focus { + border-color: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +.errorMessage { + font-size: 14px; + color: #ef4444; + margin-top: 4px; +} + +.footer { + display: flex; + justify-content: center; +} + +.submitButton { + background: #3b82f6; + color: white; + border: none; + border-radius: 8px; + padding: 12px 32px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; +} + +.submitButton:hover { + background: #2563eb; +} + +.submitButton:active { + transform: translateY(1px); +} + +.submitButton:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); +} + +@media (max-width: 640px) { + .content { + padding: 24px; + } + + .title { + font-size: 20px; + } + + .description { + font-size: 15px; + } +} \ No newline at end of file diff --git a/my-app/src/components/ui/TimeCard.module.css b/my-app/src/components/ui/TimeCard.module.css index e704566..413d2b6 100644 --- a/my-app/src/components/ui/TimeCard.module.css +++ b/my-app/src/components/ui/TimeCard.module.css @@ -26,7 +26,6 @@ .container:active, .container[data-pressed] { background-color: var(--timecard-active-bg); - transform: translateY(1px); box-shadow: var(--timecard-active-shadow); transition: all 0.1s ease; } @@ -84,7 +83,11 @@ border: 1px solid var(--timecard-unavailable-border); height: 50px; width: 165px; - background: red; +} + +.unavailableSlot:active { + background-color: var(--timecard-unavailable-bg); + box-shadow: none; } .modalFooter { @@ -142,13 +145,11 @@ .saveButton:active { background-color: var(--modal-save-active-bg); - transform: translateY(1px); box-shadow: var(--modal-save-active-shadow); } .cancelButton:active { background-color: var(--modal-cancel-active-bg); - transform: translateY(1px); } .timeSpan { diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index c538e85..f382cae 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = { earliestTimeSlot: 0, latestTimeSlot: 23, currentUserName: USER.name, + currentUserUsername: USER.username, + currentUserEmail: USER.email, showDevelopmentBanner: false, showBookingConfirmationBanner: false, showBookingDeleteBanner: false, @@ -44,8 +46,10 @@ export const SettingsProvider = ({ children }) => { ...parsed, // Convert date strings back to DateValue objects mockToday: parsed.mockToday ? new Date(parsed.mockToday) : null, - // Ensure currentUserName has a fallback + // Ensure user fields have fallbacks currentUserName: parsed.currentUserName || USER.name, + currentUserUsername: parsed.currentUserUsername || USER.username, + currentUserEmail: parsed.currentUserEmail || USER.email, }; } catch (e) { console.warn('Failed to parse saved settings:', e); @@ -55,6 +59,13 @@ export const SettingsProvider = ({ children }) => { return DEFAULT_SETTINGS; }); + // Check if user should see name prompt + const shouldShowNamePrompt = () => { + const hasSeenPrompt = localStorage.getItem('hasSeenNamePrompt'); + const hasCustomName = settings.currentUserName !== USER.name; + return !hasSeenPrompt && !hasCustomName; + }; + // Save settings to localStorage whenever they change useEffect(() => { const toSave = { @@ -93,8 +104,8 @@ export const SettingsProvider = ({ children }) => { return { id: USER.id, name: settings.currentUserName, - username: USER.username, - email: USER.email + username: settings.currentUserUsername, + email: settings.currentUserEmail }; }; @@ -112,6 +123,7 @@ export const SettingsProvider = ({ children }) => { getEffectiveToday, getCurrentUser, getDefaultBookingTitle, + shouldShowNamePrompt, }}> {children} diff --git a/my-app/src/pages/Profile.jsx b/my-app/src/pages/Profile.jsx index a2d087d..ee851e9 100644 --- a/my-app/src/pages/Profile.jsx +++ b/my-app/src/pages/Profile.jsx @@ -6,6 +6,8 @@ import styles from './Profile.module.css'; const Profile = () => { const { getCurrentUser } = useSettingsContext(); const user = getCurrentUser(); + + console.log(user, "Current User Data"); // Helper function to get user's initials const getInitials = (name) => { diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index ceb0380..77f0c6b 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -26,14 +26,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o return ( <> - {isTestSessionActive && ( -
-
-

Välkommen, {settings.currentUserName}!

-

Hantera dina bokningar och reservera nya lokaler

-
-
- )} +

Ny bokning

-- 2.39.5 From 19e63bfe2f216d82630a70d36e8e797eb2e257b8 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:41:12 +0200 Subject: [PATCH 19/20] book stort grupprum works --- my-app/src/App.jsx | 5 +- my-app/src/AppRoutes.jsx | 140 ++----------- .../components/forms/RoomSelectionField.jsx | 33 ++- my-app/src/components/layout/Navigation.jsx | 2 +- my-app/src/constants/bookingConstants.js | 45 ++++- my-app/src/context/BookingContext.jsx | 191 +++++++++++++++++- my-app/src/pages/NewBooking.jsx | 47 ++++- my-app/src/pages/RoomBooking.jsx | 4 +- 8 files changed, 326 insertions(+), 141 deletions(-) diff --git a/my-app/src/App.jsx b/my-app/src/App.jsx index e25c7d5..91ed387 100644 --- a/my-app/src/App.jsx +++ b/my-app/src/App.jsx @@ -3,6 +3,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import AppRoutes from './AppRoutes'; // move the routing and loading logic here import { SettingsProvider, useSettingsContext } from './context/SettingsContext'; import { ThemeProvider } from './context/ThemeContext'; +import { BookingsListProvider } from './context/BookingContext'; import { NamePrompt } from './components/ui/NamePrompt'; function AppContent() { @@ -34,7 +35,9 @@ function App() { return ( - + + + ); diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 6cac725..4f5d343 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -16,128 +16,31 @@ import CoursePage from './pages/CoursePage'; import Profile from './pages/Profile'; import RoomSchedules from './pages/RoomSchedules'; import { useSettingsContext } from './context/SettingsContext'; +import { useBookingsListContext } from './context/BookingContext'; const AppRoutes = () => { const location = useLocation(); const [loading, setLoading] = useState(false); - const [showSuccessBanner, setShowSuccessBanner] = useState(false); - const [lastCreatedBooking, setLastCreatedBooking] = useState(null); - const [showDeleteBanner, setShowDeleteBanner] = useState(false); - const [lastDeletedBooking, setLastDeletedBooking] = useState(null); - const [showLeaveBanner, setShowLeaveBanner] = useState(false); - const [lastLeftBooking, setLastLeftBooking] = useState(null); - const [showUpdateBanner, setShowUpdateBanner] = useState(false); - const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null); - const { getCurrentUser } = useSettingsContext(); - const currentUser = getCurrentUser(); - - // Mock bookings data - // In a real app, this would come from an API or global state - const [bookings, setBookings] = useState([ - { - id: 1, - date: new CalendarDate(2025, 9, 3), - startTime: 4, - endTime: 6, - room: 'G5:7', - roomCategory: 'green', - title: 'Team standup', - participants: [ - currentUser, - { id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' }, - { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, - { id: 4, name: 'Elin Rudling', username: 'elru4521', email: 'elin.rudling@dsv.su.se' } - ] - }, - { - id: 2, - date: new CalendarDate(2025, 9, 5), - startTime: 8, - endTime: 12, - room: 'G5:12', - roomCategory: 'red', - title: 'Project planning workshop', - participants: [ - currentUser, - { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, - { id: 6, name: 'Ellen Britschgi', username: 'elbr5623', email: 'ellen.britschgi@dsv.su.se' }, - { id: 7, name: 'Anna Andersson', username: 'anan3457', email: 'anna.andersson@dsv.su.se' }, - { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' }, - { id: 9, name: 'Sofia Karlsson', username: 'soka1245', email: 'sofia.karlsson@dsv.su.se' }, - { id: 10, name: 'Magnus Nilsson', username: 'mani6789', email: 'magnus.nilsson@dsv.su.se' } - ] - }, - { - id: 3, - date: new CalendarDate(2025, 9, 4), - startTime: 2, - endTime: 3, - room: 'G5:3', - roomCategory: 'blue', - title: '1:1 with supervisor', - participants: [ - currentUser, - { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' } - ] - }, - { - id: 4, - date: new CalendarDate(2025, 9, 6), - startTime: 6, - endTime: 8, - room: 'G5:15', - roomCategory: 'yellow', - title: 'Study group session', - participants: [ - currentUser, - { id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' }, - { id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' } - ] - }, - { - id: 5, - date: new CalendarDate(2025, 9, 7), - startTime: 10, - endTime: 12, - room: 'G5:9', - roomCategory: 'blue', - title: 'Design review meeting', - createdBy: { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, - participants: [ - { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, - currentUser, - { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, - { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' } - ], - isParticipantBooking: true - } - ]); - - function addBooking(newBooking) { - setBookings([...bookings, newBooking]); - setLastCreatedBooking(newBooking); - setShowSuccessBanner(true); - } - - function updateBooking(updatedBooking) { - setBookings(bookings.map(booking => - booking.id === updatedBooking.id ? updatedBooking : booking - )); - setLastUpdatedBooking(updatedBooking); - setShowUpdateBanner(true); - } - - function deleteBooking(bookingToDelete, actionType = 'delete') { - setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); - - if (actionType === 'leave') { - setLastLeftBooking(bookingToDelete); - setShowLeaveBanner(true); - } else { - setLastDeletedBooking(bookingToDelete); - setShowDeleteBanner(true); - } - } + + // Get bookings data and functions from context + const { + bookings, + addBooking, + updateBooking, + deleteBooking, + showSuccessBanner, + setShowSuccessBanner, + lastCreatedBooking, + showDeleteBanner, + setShowDeleteBanner, + lastDeletedBooking, + showLeaveBanner, + setShowLeaveBanner, + lastLeftBooking, + showUpdateBanner, + setShowUpdateBanner, + lastUpdatedBooking + } = useBookingsListContext(); useEffect(() => { // Reset scroll position on route change @@ -161,6 +64,7 @@ const AppRoutes = () => { }> setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} showLeaveBanner={showLeaveBanner} lastLeftBooking={lastLeftBooking} onDismissLeaveBanner={() => setShowLeaveBanner(false)} showUpdateBanner={showUpdateBanner} lastUpdatedBooking={lastUpdatedBooking} onDismissUpdateBanner={() => setShowUpdateBanner(false)} />} /> } /> + } /> } /> } /> } /> diff --git a/my-app/src/components/forms/RoomSelectionField.jsx b/my-app/src/components/forms/RoomSelectionField.jsx index 8ab3ddc..f800fea 100644 --- a/my-app/src/components/forms/RoomSelectionField.jsx +++ b/my-app/src/components/forms/RoomSelectionField.jsx @@ -4,17 +4,36 @@ import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; import styles from './RoomSelectionField.module.css'; -export function RoomSelectionField({ clean = false }) { +export function RoomSelectionField({ clean = false, roomTypeFilter = null }) { const booking = useBookingContext(); const { settings } = useSettingsContext(); - // Generate room options based on settings + // Generate room options based on settings and optional filter const roomOptions = useMemo(() => { - return Array.from({ length: settings.numberOfRooms }, (_, i) => ({ - value: `G5:${i + 1}`, - label: `G5:${i + 1}`, - })); - }, [settings.numberOfRooms]); + let rooms = []; + + if (!roomTypeFilter || roomTypeFilter === 'litet-grupprum') { + // Add small rooms G5:1-15 + rooms = rooms.concat( + Array.from({ length: settings.numberOfRooms }, (_, i) => ({ + value: `G5:${i + 1}`, + label: `G5:${i + 1}`, + })) + ); + } + + if (!roomTypeFilter || roomTypeFilter === 'stort-grupprum') { + // Add large rooms G10:1-7 + rooms = rooms.concat( + Array.from({ length: 7 }, (_, i) => ({ + value: `G10:${i + 1}`, + label: `G10:${i + 1}`, + })) + ); + } + + return rooms; + }, [settings.numberOfRooms, roomTypeFilter]); return (
diff --git a/my-app/src/components/layout/Navigation.jsx b/my-app/src/components/layout/Navigation.jsx index 3ac0ad7..a6cb6f1 100644 --- a/my-app/src/components/layout/Navigation.jsx +++ b/my-app/src/components/layout/Navigation.jsx @@ -70,7 +70,7 @@ const Navigation = () => {
- Logo + Logo Studentportalen
diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index 0627e0c..c8d9941 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -295,4 +295,47 @@ export const DEFAULT_DISABLED_OPTIONS = { 6: false, 7: false, 8: true, -}; \ No newline at end of file +}; + +export const RANDOM_BOOKING_NAMES = [ + "Projektmöte grupp 7", + "Tentapluggang", + "Metodarbete", + "Grupparbete IS1", + "Redovisning projekt", + "Handledning", + "Kravanalys meeting", + "Pluggsession statistik", + "Gruppstudier DIFO", + "Projektplanering", + "Kodgranskning", + "Retrospektiv möte", + "Scrum planning", + "Design workshop", + "Testning och debug", + "Databasdesign", + "Rapport skrivning", + "Presentationsövning", + "Peer review", + "Brainstorm session", + "Algoritm genomgång", + "UX research möte", + "Prototyping", + "Stakeholder meeting", + "Code review session", + "Agile standup", + "Requirements workshop", + "System arkitektur", + "User testing", + "Demo förberedelse", + "Krisgrupp - allt är sönder", + "Panikprogrammering", + "Buggjakt extreme edition", + "Kaffepaus (viktigt möte)", + "Stack Overflow support group", + "Deadline depression circle", + "Merge conflict therapy", + "Git blame shame session", + "Procrastination workshop", + "Är det fredag än? mötet" +]; \ No newline at end of file diff --git a/my-app/src/context/BookingContext.jsx b/my-app/src/context/BookingContext.jsx index 33966c8..08fd266 100644 --- a/my-app/src/context/BookingContext.jsx +++ b/my-app/src/context/BookingContext.jsx @@ -1,7 +1,12 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; +import { CalendarDate } from '@internationalized/date'; +import { useSettingsContext } from './SettingsContext'; +import { RANDOM_BOOKING_NAMES, PEOPLE } from '../constants/bookingConstants'; const BookingContext = createContext(null); +const BookingsListContext = createContext(null); +// Provider for individual booking forms (existing functionality) export function BookingProvider({ children, value }) { return ( @@ -10,10 +15,194 @@ export function BookingProvider({ children, value }) { ); } +// Provider for managing the list of all bookings +export function BookingsListProvider({ children }) { + const { getCurrentUser, getEffectiveToday } = useSettingsContext(); + const currentUser = getCurrentUser(); + const today = getEffectiveToday(); + + // Helper functions for random generation + const getRandomElement = (array) => array[Math.floor(Math.random() * array.length)]; + const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + + const getRoomCategory = (roomName) => { + const roomNumber = parseInt(roomName.split(':')[1]); + if (roomName.startsWith('G5:')) { + // Small rooms G5:1-15 + if (roomNumber >= 1 && roomNumber <= 4) return 'green'; + if (roomNumber >= 5 && roomNumber <= 8) return 'red'; + if (roomNumber >= 9 && roomNumber <= 12) return 'blue'; + if (roomNumber >= 13 && roomNumber <= 15) return 'yellow'; + } else if (roomName.startsWith('G10:')) { + // Large rooms G10:1-7 + return 'purple'; + } + return 'green'; + }; + + const generateRandomRoom = (isLargeRoom) => { + if (isLargeRoom) { + const roomNum = getRandomInt(1, 7); + return `G10:${roomNum}`; + } else { + const roomNum = getRandomInt(1, 15); + return `G5:${roomNum}`; + } + }; + + const generateRandomParticipants = (currentUser, isLargeRoom, isCurrentUserBooking) => { + const maxParticipants = isLargeRoom ? 10 : 5; + const minParticipants = 2; // At least creator + 1 participant + const numParticipants = getRandomInt(minParticipants, maxParticipants); + + // Get random people from PEOPLE array + const shuffledPeople = [...PEOPLE].sort(() => Math.random() - 0.5); + const selectedPeople = shuffledPeople.slice(0, numParticipants - 1); // -1 because we'll add current user or creator + + if (isCurrentUserBooking) { + // Current user is the creator, add them first + return [currentUser, ...selectedPeople]; + } else { + // Someone else is the creator, current user may or may not be a participant + const creator = selectedPeople[0]; + const otherParticipants = selectedPeople.slice(1); + + // 50% chance current user is a participant in other people's bookings + if (Math.random() < 0.5) { + return [creator, currentUser, ...otherParticipants]; + } else { + return [creator, ...otherParticipants]; + } + } + }; + + // Generate bookings relative to current date + const generateInitialBookings = () => { + const numBookings = getRandomInt(2, 10); + const bookings = []; + + for (let i = 0; i < numBookings; i++) { + const isLargeRoom = Math.random() < 0.2; // 20% chance of large room + const isCurrentUserBooking = Math.random() > 0.2; // 80% current user's bookings, 20% others' + + const room = generateRandomRoom(isLargeRoom); + const participants = generateRandomParticipants(currentUser, isLargeRoom, isCurrentUserBooking); + const creator = participants[0]; + + // Random date within next 14 days + const daysFromNow = getRandomInt(0, 13); + const date = today.add({ days: daysFromNow }); + + // Random time slots (8:00-17:30, in 30-minute slots) + const startTimeSlot = getRandomInt(0, 19); // 8:00-17:30 + const duration = getRandomInt(1, Math.min(4, 19 - startTimeSlot)); // 30min to 2h, don't exceed 17:30 + const endTimeSlot = startTimeSlot + duration; + + const booking = { + id: i + 1, + date: date, + startTime: startTimeSlot, + endTime: endTimeSlot, + room: room, + roomCategory: getRoomCategory(room), + title: getRandomElement(RANDOM_BOOKING_NAMES), + participants: participants, + createdBy: creator, + isParticipantBooking: !isCurrentUserBooking && participants.includes(currentUser) + }; + + bookings.push(booking); + } + + // Sort by date and time + return bookings.sort((a, b) => { + if (a.date.compare(b.date) !== 0) { + return a.date.compare(b.date); + } + return a.startTime - b.startTime; + }); + }; + + // Initial bookings data (relative to current date) + const [bookings, setBookings] = useState(() => generateInitialBookings()); + + // Banner states (moved from AppRoutes) + const [showSuccessBanner, setShowSuccessBanner] = useState(false); + const [lastCreatedBooking, setLastCreatedBooking] = useState(null); + const [showDeleteBanner, setShowDeleteBanner] = useState(false); + const [lastDeletedBooking, setLastDeletedBooking] = useState(null); + const [showLeaveBanner, setShowLeaveBanner] = useState(false); + const [lastLeftBooking, setLastLeftBooking] = useState(null); + const [showUpdateBanner, setShowUpdateBanner] = useState(false); + const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null); + + // Booking management functions (moved from AppRoutes) + function addBooking(newBooking) { + setBookings([...bookings, newBooking]); + setLastCreatedBooking(newBooking); + setShowSuccessBanner(true); + } + + function updateBooking(updatedBooking) { + setBookings(bookings.map(booking => + booking.id === updatedBooking.id ? updatedBooking : booking + )); + setLastUpdatedBooking(updatedBooking); + setShowUpdateBanner(true); + } + + function deleteBooking(bookingToDelete, actionType = 'delete') { + setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); + + if (actionType === 'leave') { + setLastLeftBooking(bookingToDelete); + setShowLeaveBanner(true); + } else { + setLastDeletedBooking(bookingToDelete); + setShowDeleteBanner(true); + } + } + + const value = { + bookings, + addBooking, + updateBooking, + deleteBooking, + showSuccessBanner, + setShowSuccessBanner, + lastCreatedBooking, + showDeleteBanner, + setShowDeleteBanner, + lastDeletedBooking, + showLeaveBanner, + setShowLeaveBanner, + lastLeftBooking, + showUpdateBanner, + setShowUpdateBanner, + lastUpdatedBooking + }; + + return ( + + {children} + + ); +} + +// Hook for individual booking forms (existing functionality) export function useBookingContext() { const context = useContext(BookingContext); if (!context) { throw new Error('useBookingContext must be used within a BookingProvider'); } return context; +} + +// Hook for accessing the list of all bookings +export function useBookingsListContext() { + const context = useContext(BookingsListContext); + if (!context) { + throw new Error('useBookingsListContext must be used within a BookingsListProvider'); + } + return context; } \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index e05b0cd..052f9c3 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/ui/TimeCardContainer'; import { BookingDatePicker } from '../components/forms/BookingDatePicker'; @@ -12,6 +13,7 @@ import PageContainer from '../components/layout/PageContainer'; import Breadcrumbs from '../components/ui/Breadcrumbs'; export function NewBooking({ addBooking }) { + const { roomType } = useParams(); const { getEffectiveToday, settings } = useSettingsContext(); const booking = useBookingState(addBooking, getEffectiveToday()); const [showFilters, setShowFilters] = useState(false); @@ -23,6 +25,31 @@ export function NewBooking({ addBooking }) { // Check if we should use inline form (hide title and participants from main form) const useInlineForm = settings.bookingFormType === 'inline'; + // Get page title, subtitle, and image based on room type + const getPageInfo = () => { + if (roomType === 'litet-grupprum') { + return { + title: 'Litet grupprum', + subtitle: 'Plats för upp till 5 personer', + imageUrl: '/grupprum.jpg' + }; + } else if (roomType === 'stort-grupprum') { + return { + title: 'Stort grupprum', + subtitle: 'Plats för upp till 10 personer', + imageUrl: '/stort-grupprum.jpg' + }; + } else { + return { + title: 'Boka grupprum', + subtitle: 'Välj från tillgängliga rum', + imageUrl: '/grupprum.jpg' + }; + } + }; + + const { title: pageTitle, subtitle: pageSubtitle, imageUrl } = getPageInfo(); + // Check if any filters are active const hasActiveFilters = booking.selectedRoom !== "allRooms" || booking.selectedBookingLength > 0; @@ -72,9 +99,9 @@ export function NewBooking({ addBooking }) { <>
} />
@@ -92,7 +119,7 @@ export function NewBooking({ addBooking }) { /* Always-visible filters */
- +
@@ -113,7 +140,7 @@ export function NewBooking({ addBooking }) { {showFilters && (
- +
{hasActiveFilters && ( @@ -149,9 +176,9 @@ export function NewBooking({ addBooking }) { /* Stacked layout (original) */ <> } />
@@ -167,7 +194,7 @@ export function NewBooking({ addBooking }) { /* Always-visible filters */
- +
@@ -188,7 +215,7 @@ export function NewBooking({ addBooking }) { {showFilters && (
- +
{hasActiveFilters && ( diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 77f0c6b..4f0515d 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -32,10 +32,10 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o

Ny bokning

- + - +
-- 2.39.5 From 8932bcbfedc5e115131bf4629096f3e27759640a Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:49:49 +0200 Subject: [PATCH 20/20] preparing tests --- my-app/bookings-2025-09-29.json | 492 ++++++++++++++++++ my-app/bookings-2025-09-30.json | 447 ++++++++++++++++ .../components/forms/RoomSelectionField.jsx | 4 +- .../src/components/ui/TimeCardContainer.jsx | 1 + my-app/src/constants/bookingConstants.js | 2 + my-app/src/context/SettingsContext.jsx | 11 +- my-app/src/pages/BookingSettings.jsx | 2 +- my-app/src/utils/bookingUtils.js | 6 + 8 files changed, 960 insertions(+), 5 deletions(-) create mode 100644 my-app/bookings-2025-09-29.json create mode 100644 my-app/bookings-2025-09-30.json diff --git a/my-app/bookings-2025-09-29.json b/my-app/bookings-2025-09-29.json new file mode 100644 index 0000000..e3e8e09 --- /dev/null +++ b/my-app/bookings-2025-09-29.json @@ -0,0 +1,492 @@ +{ + "date": "2025-09-29", + "dayName": "måndag", + "bookings": [ + { + "id": 1, + "room": "G10:1", + "time": "08:00-10:00", + "startTime": 0, + "endTime": 4, + "title": "study", + "details": "tid: 08-10" + }, + { + "id": 2, + "room": "G10:2", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "REQ", + "details": "tid: 09-13" + }, + { + "id": 3, + "room": "G10:3", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "plugg", + "details": "tid: 10-12" + }, + { + "id": 4, + "room": "G10:4", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "Välkommen välkommen", + "details": "tid: 09-13" + }, + { + "id": 5, + "room": "G10:5", + "time": "13:00-17:00", + "startTime": 10, + "endTime": 18, + "title": "REQ", + "details": "tid: 13-17" + }, + { + "id": 6, + "room": "G10:6", + "time": "13:00-14:00", + "startTime": 10, + "endTime": 12, + "title": "FEEWC", + "details": "tid: 13-14" + }, + { + "id": 7, + "room": "G10:6", + "time": "14:00-16:00", + "startTime": 12, + "endTime": 16, + "title": "metod", + "details": "tid: 14-16" + }, + { + "id": 8, + "room": "G10:7", + "time": "10:00-13:00", + "startTime": 4, + "endTime": 10, + "title": "Project Management Team 5", + "details": "tid: 10-13" + }, + { + "id": 9, + "room": "G10:7", + "time": "14:00-18:00", + "startTime": 12, + "endTime": 20, + "title": "Tenta plugg", + "details": "tid: 14-18" + }, + { + "id": 10, + "room": "G5:1", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "Plugg G25", + "details": "tid: 10-12" + }, + { + "id": 11, + "room": "G5:10", + "time": "08:00-11:00", + "startTime": 0, + "endTime": 6, + "title": "Grupp 43A", + "details": "tid: 08-11" + }, + { + "id": 12, + "room": "G5:11", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "Plottest", + "details": "tid: 11-15" + }, + { + "id": 13, + "room": "G5:12", + "time": "08:00-10:00", + "startTime": 0, + "endTime": 4, + "title": "Study", + "details": "tid: 08-10" + }, + { + "id": 14, + "room": "G5:12", + "time": "12:00-14:00", + "startTime": 8, + "endTime": 12, + "title": "metod", + "details": "tid: 12-14" + }, + { + "id": 15, + "room": "G5:13", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "DSB Grupp 25 krav intervju", + "details": "tid: 09-13" + }, + { + "id": 16, + "room": "G5:13", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "Project Group meeting", + "details": "tid: 13-15" + }, + { + "id": 17, + "room": "G5:13", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "Examensarbete", + "details": "tid: 15-17" + }, + { + "id": 18, + "room": "G5:14", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "KP Plugg Krav", + "details": "tid: 09-12" + }, + { + "id": 19, + "room": "G5:14", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "REQ Grupp 5", + "details": "tid: 12-16" + }, + { + "id": 20, + "room": "G5:14", + "time": "16:00-17:00", + "startTime": 16, + "endTime": 18, + "title": "Metod g6", + "details": "tid: 16-17" + }, + { + "id": 21, + "room": "G5:15", + "time": "09:00-14:00", + "startTime": 2, + "endTime": 12, + "title": "Krav grupp", + "details": "tid: 09-14" + }, + { + "id": 22, + "room": "G5:15", + "time": "13:00-14:00", + "startTime": 10, + "endTime": 12, + "title": "Stats", + "details": "tid: 13-14" + }, + { + "id": 23, + "room": "G5:15", + "time": "14:00-17:00", + "startTime": 12, + "endTime": 18, + "title": "Plugg TRIO", + "details": "tid: 14-17" + }, + { + "id": 24, + "room": "G5:16", + "time": "11:00-13:00", + "startTime": 6, + "endTime": 10, + "title": "plugggg", + "details": "tid: 11-13" + }, + { + "id": 25, + "room": "G5:17", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "Experiment KP G4", + "details": "tid: 09-12" + }, + { + "id": 26, + "room": "G5:17", + "time": "14:00-17:00", + "startTime": 12, + "endTime": 18, + "title": "plugg", + "details": "tid: 14-17" + }, + { + "id": 27, + "room": "G5:2", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "Grups testning", + "details": "tid: 12-16" + }, + { + "id": 28, + "room": "G5:3", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "Experiment KP G4", + "details": "tid: 09-12" + }, + { + "id": 29, + "room": "G5:3", + "time": "10:00-14:00", + "startTime": 4, + "endTime": 12, + "title": "KRAV meeting", + "details": "tid: 10-14" + }, + { + "id": 30, + "room": "G5:4", + "time": "14:00-16:00", + "startTime": 12, + "endTime": 16, + "title": "P", + "details": "tid: 14-16" + }, + { + "id": 31, + "room": "G5:5", + "time": "10:00-14:00", + "startTime": 4, + "endTime": 12, + "title": "msb METOD 38", + "details": "tid: 10-14" + }, + { + "id": 32, + "room": "G5:6", + "time": "10:00-13:00", + "startTime": 4, + "endTime": 10, + "title": "Metod gr 50 REQ Group 5", + "details": "tid: 10-13" + }, + { + "id": 33, + "room": "G5:6", + "time": "11:00-13:00", + "startTime": 6, + "endTime": 10, + "title": "REQ Group 5", + "details": "tid: 11-13" + }, + { + "id": 34, + "room": "G5:7", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "LOCKED IN KRAV", + "details": "tid: 13-15" + }, + { + "id": 35, + "room": "G5:8", + "time": "13:00-17:00", + "startTime": 10, + "endTime": 18, + "title": "REQ Study", + "details": "tid: 13-17" + }, + { + "id": 36, + "room": "G5:8", + "time": "15:00-18:00", + "startTime": 14, + "endTime": 20, + "title": "plugg", + "details": "tid: 15-18" + }, + { + "id": 37, + "room": "G5:9", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "Study", + "details": "tid: 13-15" + }, + { + "id": 38, + "room": "G5:9", + "time": "14:00-18:00", + "startTime": 12, + "endTime": 20, + "title": "Stats Plugg", + "details": "tid: 14-18" + }, + { + "id": 39, + "room": "G10:1", + "time": "10:00-13:00", + "startTime": 4, + "endTime": 10, + "title": "Test KP grupp 1", + "details": "tid: 10-13" + }, + { + "id": 40, + "room": "G10:1", + "time": "12:00-15:00", + "startTime": 8, + "endTime": 14, + "title": "METOD grupp 37", + "details": "tid: 12-15" + }, + { + "id": 41, + "room": "G10:1", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "G25 Krav Group meeting", + "details": "tid: 13-15" + }, + { + "id": 42, + "room": "G10:1", + "time": "15:00-16:00", + "startTime": 14, + "endTime": 16, + "title": "IS plugg session", + "details": "tid: 15-16" + }, + { + "id": 43, + "room": "G10:2", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "ERP dsb test", + "details": "tid: 15-17" + }, + { + "id": 44, + "room": "G10:4", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "Group L", + "details": "tid: 13-15" + }, + { + "id": 45, + "room": "G10:5", + "time": "17:00-21:00", + "startTime": 18, + "endTime": 26, + "title": "Kati se getires :)", + "details": "tid: 17-21" + }, + { + "id": 46, + "room": "G5:11", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "Meeting", + "details": "tid: 13-15" + }, + { + "id": 47, + "room": "G5:12", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "Grupp", + "details": "tid: 10-12" + }, + { + "id": 48, + "room": "G5:13", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "Sifu", + "details": "tid: 15-17" + }, + { + "id": 49, + "room": "G5:14", + "time": "14:00-17:00", + "startTime": 12, + "endTime": 18, + "title": "Plugg TRIO", + "details": "tid: 14-17" + }, + { + "id": 50, + "room": "G5:15", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "working", + "details": "tid: 15-17" + }, + { + "id": 51, + "room": "G5:16", + "time": "14:00-17:00", + "startTime": 12, + "endTime": 18, + "title": "IS 1 session", + "details": "tid: 14-17" + }, + { + "id": 52, + "room": "G10:1", + "time": "17:00-20:00", + "startTime": 18, + "endTime": 24, + "title": "SUPCOM DB", + "details": "tid: 17-20" + }, + { + "id": 53, + "room": "G5:12", + "time": "16:00-16:00", + "startTime": 16, + "endTime": 16, + "title": "DIFO grupp 43", + "details": "tid: 12-16" + }, + { + "id": 54, + "room": "G5:11", + "time": "15:00-18:00", + "startTime": 14, + "endTime": 20, + "title": "Projektarbete Meeting", + "details": "tid: 15-18" + } + ] +} \ No newline at end of file diff --git a/my-app/bookings-2025-09-30.json b/my-app/bookings-2025-09-30.json new file mode 100644 index 0000000..2ddb747 --- /dev/null +++ b/my-app/bookings-2025-09-30.json @@ -0,0 +1,447 @@ +{ + "date": "2025-09-30", + "dayName": "tisdag", + "bookings": [ + { + "id": 1, + "room": "G10:1", + "time": "08:00-12:00", + "startTime": 0, + "endTime": 8, + "title": "välkommen 2", + "details": "tid: 08-12" + }, + { + "id": 2, + "room": "G10:2", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "Grupp 1", + "details": "tid: 09-13" + }, + { + "id": 3, + "room": "G10:3", + "time": "08:00-12:00", + "startTime": 0, + "endTime": 8, + "title": "Välkommen", + "details": "tid: 08-12" + }, + { + "id": 4, + "room": "G10:4", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "Plugg - LOGIK & SAK1", + "details": "tid: 09-13" + }, + { + "id": 5, + "room": "G10:5", + "time": "10:00-14:00", + "startTime": 4, + "endTime": 12, + "title": "KRAV grupp 15a", + "details": "tid: 10-14" + }, + { + "id": 6, + "room": "G10:6", + "time": "10:00-11:00", + "startTime": 4, + "endTime": 6, + "title": "Läsgrupp 8", + "details": "tid: 10-11" + }, + { + "id": 7, + "room": "G10:6", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "p", + "details": "tid: 11-15" + }, + { + "id": 8, + "room": "G10:7", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "Introduktion Kreativ Bokföring", + "details": "tid: 09-12" + }, + { + "id": 9, + "room": "G10:7", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "G25 Krav", + "details": "tid: 15-17" + }, + { + "id": 10, + "room": "G5:1", + "time": "09:00-11:00", + "startTime": 2, + "endTime": 6, + "title": "Drop-in studieavvägledning", + "details": "tid: 09-30-11" + }, + { + "id": 11, + "room": "G5:10", + "time": "09:00-11:00", + "startTime": 2, + "endTime": 6, + "title": "Grou 05a", + "details": "tid: 09-11" + }, + { + "id": 12, + "room": "G5:10", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "Ej bokningsbar DIFO labb", + "details": "tid: 11-11:30, tid: 11-15" + }, + { + "id": 13, + "room": "G5:11", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "KRAVlösa", + "details": "tid: 11-15" + }, + { + "id": 14, + "room": "G5:12", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "REQ Group 5", + "details": "tid: 09-12" + }, + { + "id": 15, + "room": "G5:12", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "Grupp 34 Projektarbete", + "details": "tid: 12-16" + }, + { + "id": 16, + "room": "G5:13", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "Metod 46", + "details": "tid: 10-12" + }, + { + "id": 17, + "room": "G5:14", + "time": "09:00-10:00", + "startTime": 2, + "endTime": 4, + "title": "ERP Group work", + "details": "tid: 09-10" + }, + { + "id": 18, + "room": "G5:14", + "time": "10:00-14:00", + "startTime": 4, + "endTime": 12, + "title": "metod", + "details": "tid: 10-14" + }, + { + "id": 19, + "room": "G5:14", + "time": "14:00-15:00", + "startTime": 12, + "endTime": 14, + "title": "Group AE", + "details": "tid: 14-15" + }, + { + "id": 20, + "room": "G5:15", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "Plugg TRIO", + "details": "tid: 09-13" + }, + { + "id": 21, + "room": "G5:15", + "time": "14:00-17:00", + "startTime": 12, + "endTime": 18, + "title": "Plugg TRIO", + "details": "tid: 14-17" + }, + { + "id": 22, + "room": "G5:16", + "time": "09:00-12:00", + "startTime": 2, + "endTime": 8, + "title": "User testing", + "details": "tid: 09-12" + }, + { + "id": 23, + "room": "G5:17", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "sah", + "details": "tid: 10-12" + }, + { + "id": 24, + "room": "G5:17", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "plugg KRAV projektarbete", + "details": "tid: 12-16, tid: 12-13" + }, + { + "id": 25, + "room": "G5:17", + "time": "13:00-16:00", + "startTime": 10, + "endTime": 16, + "title": "Krav", + "details": "tid: 13-16" + }, + { + "id": 26, + "room": "G5:2", + "time": "10:00-14:00", + "startTime": 4, + "endTime": 12, + "title": "APID G4 Experiment", + "details": "tid: 10-14" + }, + { + "id": 27, + "room": "G5:2", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "Group 8", + "details": "tid: 11-15" + }, + { + "id": 28, + "room": "G5:2", + "time": "14:00-15:00", + "startTime": 12, + "endTime": 14, + "title": "Sonic", + "details": "tid: 14-15" + }, + { + "id": 29, + "room": "G5:2", + "time": "15:00-19:00", + "startTime": 14, + "endTime": 22, + "title": "plugg", + "details": "tid: 15-19" + }, + { + "id": 30, + "room": "G5:3", + "time": "09:00-13:00", + "startTime": 2, + "endTime": 10, + "title": "plugg", + "details": "tid: 09-13" + }, + { + "id": 31, + "room": "G5:4", + "time": "10:00-13:00", + "startTime": 4, + "endTime": 10, + "title": "msb IS1 Group 41", + "details": "tid: 10-13" + }, + { + "id": 32, + "room": "G5:4", + "time": "11:00-15:00", + "startTime": 6, + "endTime": 14, + "title": "Plugg", + "details": "tid: 11-15" + }, + { + "id": 33, + "room": "G5:5", + "time": "10:00-12:00", + "startTime": 4, + "endTime": 8, + "title": "Stats", + "details": "tid: 10-12" + }, + { + "id": 34, + "room": "G5:5", + "time": "12:00-13:00", + "startTime": 8, + "endTime": 10, + "title": "plugg", + "details": "tid: 12-13" + }, + { + "id": 35, + "room": "G5:6", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "MAJO assignemnt", + "details": "tid: 13-15" + }, + { + "id": 36, + "room": "G5:6", + "time": "13:00-17:00", + "startTime": 10, + "endTime": 18, + "title": "plugg Group 21", + "details": "tid: 13-17, tid: 13-15" + }, + { + "id": 37, + "room": "G5:7", + "time": "14:00-16:00", + "startTime": 12, + "endTime": 16, + "title": "Stats", + "details": "tid: 14-16" + }, + { + "id": 38, + "room": "G10:1", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "plugg", + "details": "tid: 12-16" + }, + { + "id": 39, + "room": "G10:2", + "time": "13:00-16:00", + "startTime": 10, + "endTime": 16, + "title": "KRAV grupp 33A", + "details": "tid: 12-16" + }, + { + "id": 40, + "room": "G10:3", + "time": "15:00-17:00", + "startTime": 14, + "endTime": 18, + "title": "pluggiluring :D", + "details": "tid: 15-17" + }, + { + "id": 41, + "room": "G10:4", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "G25 Krav", + "details": "tid: 13-15" + }, + { + "id": 42, + "room": "G10:5", + "time": "14:00-18:00", + "startTime": 12, + "endTime": 20, + "title": "KRAV Plugg", + "details": "tid: 14-18" + }, + { + "id": 43, + "room": "G10:6", + "time": "15:00-18:00", + "startTime": 14, + "endTime": 20, + "title": "TinyML project work", + "details": "tid: 15-18" + }, + { + "id": 44, + "room": "G10:7", + "time": "15:00-18:00", + "startTime": 14, + "endTime": 20, + "title": "ERP Meeting", + "details": "tid: 15-18" + }, + { + "id": 45, + "room": "G10:1", + "time": "16:00-19:00", + "startTime": 16, + "endTime": 22, + "title": "Plugg", + "details": "tid: 16-19" + }, + { + "id": 46, + "room": "G5:11", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "Krav grupp grupparbete", + "details": "tid: 12-16" + }, + { + "id": 47, + "room": "G5:12", + "time": "12:00-16:00", + "startTime": 8, + "endTime": 16, + "title": "HUGO", + "details": "tid: 12-16" + }, + { + "id": 48, + "room": "G5:13", + "time": "13:00-14:00", + "startTime": 10, + "endTime": 12, + "title": "MAJO AE", + "details": "tid: 13-14" + }, + { + "id": 49, + "room": "G5:8", + "time": "13:00-15:00", + "startTime": 10, + "endTime": 14, + "title": "IS1 plugg Group 21", + "details": "tid: 13-15" + } + ] +} \ No newline at end of file diff --git a/my-app/src/components/forms/RoomSelectionField.jsx b/my-app/src/components/forms/RoomSelectionField.jsx index f800fea..130c301 100644 --- a/my-app/src/components/forms/RoomSelectionField.jsx +++ b/my-app/src/components/forms/RoomSelectionField.jsx @@ -15,7 +15,7 @@ export function RoomSelectionField({ clean = false, roomTypeFilter = null }) { if (!roomTypeFilter || roomTypeFilter === 'litet-grupprum') { // Add small rooms G5:1-15 rooms = rooms.concat( - Array.from({ length: settings.numberOfRooms }, (_, i) => ({ + Array.from({ length: settings.numberOfSmallGroupRooms }, (_, i) => ({ value: `G5:${i + 1}`, label: `G5:${i + 1}`, })) @@ -25,7 +25,7 @@ export function RoomSelectionField({ clean = false, roomTypeFilter = null }) { if (!roomTypeFilter || roomTypeFilter === 'stort-grupprum') { // Add large rooms G10:1-7 rooms = rooms.concat( - Array.from({ length: 7 }, (_, i) => ({ + Array.from({ length: settings.numberOfLargeGroupRooms }, (_, i) => ({ value: `G10:${i + 1}`, label: `G10:${i + 1}`, })) diff --git a/my-app/src/components/ui/TimeCardContainer.jsx b/my-app/src/components/ui/TimeCardContainer.jsx index af149f7..e876c9b 100644 --- a/my-app/src/components/ui/TimeCardContainer.jsx +++ b/my-app/src/components/ui/TimeCardContainer.jsx @@ -277,6 +277,7 @@ export function TimeCardContainer({ addBooking, forceOneColumn = false }) { const renderTimeCard = (slotIndex) => { let maxConsecutive = 0; let roomId = ""; + console.log(booking); if (booking.currentRoom) { const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex); diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index c8d9941..fc38fa0 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -1,4 +1,6 @@ export const NUMBER_OF_ROOMS = 15; +export const NUMBER_OF_SMALL_GROUP_ROOMS = 17; +export const NUMBER_OF_LARGE_GROUP_ROOMS = 7; export const CHANCE_OF_AVAILABILITY = 0.5; export const DEFAULT_BOOKING_TITLE = "Jacobs bokning"; diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index f382cae..2d630da 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -1,6 +1,11 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { today, getLocalTimeZone, CalendarDate } from '@internationalized/date'; -import { USER } from '../constants/bookingConstants'; +import { + USER, + CHANCE_OF_AVAILABILITY, + NUMBER_OF_SMALL_GROUP_ROOMS, + NUMBER_OF_LARGE_GROUP_ROOMS, + } from '../constants/bookingConstants'; const SettingsContext = createContext(); @@ -8,8 +13,10 @@ const SettingsContext = createContext(); const DEFAULT_SETTINGS = { mockToday: null, bookingRangeDays: 14, - roomAvailabilityChance: 0.7, + roomAvailabilityChance: CHANCE_OF_AVAILABILITY, numberOfRooms: 5, + numberOfSmallGroupRooms: NUMBER_OF_SMALL_GROUP_ROOMS, + numberOfLargeGroupRooms: NUMBER_OF_LARGE_GROUP_ROOMS, earliestTimeSlot: 0, latestTimeSlot: 23, currentUserName: USER.name, diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index d3f5d45..c47b494 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -250,7 +250,7 @@ export function BookingSettings() {
Inline Expansion: Card expands directly in the list for editing
Modal Popup: Always opens in a centered modal dialog
- Responsive: Inline on mobile (≤780px), modal on desktop (>780px) + Responsive: Inline on mobile (≤780px), modal on desktop (>780px)
diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index fec1d7b..5dd03e9 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -2,6 +2,12 @@ import React from 'react'; import { today, getLocalTimeZone } from '@internationalized/date'; import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY, USER } from '../constants/bookingConstants'; +// Simple seeded random number generator +const seededRandom = (seed) => { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); +}; + export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 23) => { return [...Array(numberOfRooms)].map((room, index) => ({ roomId: `G5:${index + 1}`, -- 2.39.5