diff --git a/my-app/src/App.jsx b/my-app/src/App.jsx index 5ec76de..dd45cca 100644 --- a/my-app/src/App.jsx +++ b/my-app/src/App.jsx @@ -1,11 +1,15 @@ +import React 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'; function App() { return ( - - - + + + + + ); } diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 5c7189f..1879d74 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -1,20 +1,102 @@ +import React from 'react'; import { Routes, Route, useLocation } from 'react-router-dom'; import { useEffect, useState } from 'react'; +import { CalendarDate } from '@internationalized/date'; import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; +import { BookingSettings } from './pages/BookingSettings'; +import { TestSession } from './pages/TestSession'; import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { const location = useLocation(); const [loading, setLoading] = useState(false); - const [bookings, setBookings] = useState([]); + const [showSuccessBanner, setShowSuccessBanner] = useState(false); + const [lastCreatedBooking, setLastCreatedBooking] = useState(null); + const [showDeleteBanner, setShowDeleteBanner] = useState(false); + const [lastDeletedBooking, setLastDeletedBooking] = useState(null); + 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: [ + { 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: [ + { 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: [ + { 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: [ + { 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' } + ] + } + ]); function addBooking(newBooking) { setBookings([...bookings, newBooking]); + setLastCreatedBooking(newBooking); + setShowSuccessBanner(true); + } + + function updateBooking(updatedBooking) { + setBookings(bookings.map(booking => + booking.id === updatedBooking.id ? updatedBooking : booking + )); + } + + function deleteBooking(bookingToDelete) { + setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); + setLastDeletedBooking(bookingToDelete); + setShowDeleteBanner(true); } useEffect(() => { + // Reset scroll position on route change + window.scrollTo(0, 0); + setLoading(true); const timer = setTimeout(() => setLoading(false), 800); return () => clearTimeout(timer); @@ -23,12 +105,17 @@ const AppRoutes = () => { return ( <> {/* Pass loading as isVisible to FullScreenLoader */} - + {/**/} + + {/* Fullscreen route outside of Layout */} + } /> + }> - } /> + setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} /> } /> + } /> diff --git a/my-app/src/Layout.jsx b/my-app/src/Layout.jsx index 76c608f..8b4bf89 100644 --- a/my-app/src/Layout.jsx +++ b/my-app/src/Layout.jsx @@ -1,4 +1,5 @@ // components/Layout.jsx +import React from 'react'; import { Outlet, Link } from 'react-router-dom'; import Header from './components/Header'; diff --git a/my-app/src/components/Booking.jsx b/my-app/src/components/Booking.jsx deleted file mode 100644 index f5ab816..0000000 --- a/my-app/src/components/Booking.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import styles from './Booking.module.css'; -import { convertDateObjectToString } from '../helpers'; - -function Booking({ booking, handleEditBooking }) { - 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 ( -
handleEditBooking(booking)}> -
-

{convertDateObjectToString(booking.date)}

-

{booking.title}

-
-
-

{booking.room}

-

{getTimeFromIndex(booking.startTime)} - {getTimeFromIndex(booking.endTime)}

-
-
- ); -} - -export default Booking; \ No newline at end of file diff --git a/my-app/src/components/Booking.module.css b/my-app/src/components/Booking.module.css deleted file mode 100644 index df33613..0000000 --- a/my-app/src/components/Booking.module.css +++ /dev/null @@ -1,44 +0,0 @@ -.container { - display: flex; - justify-content: space-between; - border: 1px solid #E5E5E5; - padding: 0.7rem; - width: 100%; - border-radius: 0.5rem; -} - -.left { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.right { - display: flex; - flex-direction: column; - align-items: end; -} - -.container p { - margin: 0; -} - -.container:hover { - cursor: pointer; -} - -.date { - text-transform: uppercase; - font-size: 0.8rem; -} - -.room { - font-weight: 600; - font-size: 0.8rem; - color: #5d5d5d; -} - -.time { - font-size: 1.2rem; -} \ No newline at end of file diff --git a/my-app/src/components/BookingCard.jsx b/my-app/src/components/BookingCard.jsx new file mode 100644 index 0000000..f065ae8 --- /dev/null +++ b/my-app/src/components/BookingCard.jsx @@ -0,0 +1,248 @@ +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 './Dropdown'; +import { BookingTitleField } from './BookingTitleField'; +import { ParticipantsSelector } from './ParticipantsSelector'; +import { BookingProvider } from '../context/BookingContext'; +import { PEOPLE } from '../constants/bookingConstants'; + +function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) { + 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]); + + // 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) + ); + } + }; + + 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 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), + }; + + function handleLengthChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = booking.startTime + lengthValue; + setCalculatedEndTime(newEndTime); + } else { + setCalculatedEndTime(booking.endTime); + } + } + + function handleSave() { + if (selectedLength !== null && 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 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 getRoomCategoryClass(category) { + return `room-${category}`; + } + + + function formatParticipants(participants) { + if (!participants || participants.length === 0) return null; + + if (participants.length === 1) { + return participants[0].name; + } else if (participants.length === 2) { + return `${participants[0].name} and ${participants[1].name}`; + } else { + const remaining = participants.length - 2; + return `${participants[0].name}, ${participants[1].name} and ${remaining} more`; + } + } + + return ( +
+
+
+ {convertDateObjectToString(booking.date)} +
+

{booking.title}

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

{formatParticipants(booking.participants)}

+ )} +
+
+
{getTimeFromIndex(booking.startTime)}
+
{getTimeFromIndex(calculatedEndTime || booking.endTime)}
+
+
+ + {isExpanded && ( + +
+
+ +
+ +
+ +
+ +
+ + +
+ + {!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)} +

+
+
+ + +
+
+ )} +
+
+ )} +
+ ); +} + +export default BookingCard; \ No newline at end of file diff --git a/my-app/src/components/BookingCard.module.css b/my-app/src/components/BookingCard.module.css new file mode 100644 index 0000000..eacd6b3 --- /dev/null +++ b/my-app/src/components/BookingCard.module.css @@ -0,0 +1,480 @@ +.card { + border: 1px solid #E5E5E5; + padding: 1.25rem; + width: 100%; + border-radius: 0.5rem; + background: #fff; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.card:hover { + cursor: pointer; + border-color: #007AFF; + box-shadow: 0 4px 16px rgba(0, 122, 255, 0.12); + transform: translateY(-2px); +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + height: 100%; +} + +.leftSection { + flex: 1; +} + +.date { + text-transform: uppercase; + font-size: 0.8rem; + font-weight: 600; + color: #999; + letter-spacing: 1px; + margin-bottom: 0.5rem; + display: block; +} + +.titleRow { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.title { + margin: 0; + font-size: 1.25rem; + font-weight: 700; + color: #333; + line-height: 1.3; +} + +.room { + font-weight: 600; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + border-radius: 1rem; + white-space: nowrap; +} + +.room-green { + background: #D4EDDA; + color: #155724; +} + +.room-red { + background: #F8D7DA; + color: #721C24; +} + +.room-blue { + background: #D1ECF1; + color: #0C5460; +} + +.room-yellow { + background: #FFF3CD; + color: #856404; +} + +.timeSection { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + /*background-color:rgba(0, 122, 255, 0.12);*/ + min-height: 80px; + gap: 0.2rem; +} + +.startTime { + font-size: 1.6rem; + font-weight: 400; + color: #333; + line-height: 1; +} + +.endTime { + font-size: 1.6rem; + font-weight: 400; + color: #acacac; + line-height: 1; +} + +.participants { + margin: 0; + font-size: 0.9rem; + color: #999; +} + +/* Expanded card styles */ +.expanded { + border-color: #007AFF; + box-shadow: 0 4px 16px rgba(0, 122, 255, 0.12); +} + +.expanded:hover { + transform: none; +} + +.expanded .header { + cursor: default; +} + +.expandedContent { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #E5E5E5; +} + +.formSection { + margin-bottom: 1.5rem; +} + +.editSection { + margin-bottom: 1.5rem; +} + +.label { + display: block; + font-size: 0.8rem; + color: #717171; + font-weight: 500; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.buttonSection { + display: flex; + gap: 1rem; + align-items: center; +} + +.deleteButton { + flex: 2; + background-color: #DC2626; + color: white; + height: 3rem; + font-weight: 600; + font-size: 0.9rem; + border: 2px solid #B91C1C; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2); + cursor: pointer; +} + +.deleteButton:hover { + background-color: #B91C1C; + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +.deleteButton:active { + background-color: #991B1B; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(220, 38, 38, 0.2); +} + +.cancelButton { + flex: 2; + background-color: white; + height: 3rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.5rem; + transition: all 0.2s ease; + cursor: pointer; + font-size: 0.9rem; +} + +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.saveButton { + flex: 3; + background-color: #059669; + color: white; + height: 3rem; + font-weight: 600; + font-size: 0.95rem; + border: 2px solid #047857; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); + cursor: pointer; +} + +.saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); +} + +.saveButton:active { + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); +} + +.saveButton[data-focused], +.cancelButton[data-focused], +.deleteButton[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} + +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + opacity: 0.6 !important; + box-shadow: none !important; + cursor: default !important; +} + +.disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none !important; +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; +} + +/* Compact form inputs */ +.compactInput { + width: 100%; + padding: 0.5rem 1rem; + border: 1px solid #ccc; + border-radius: 0.375rem; + font-size: 1rem; + background-color: white; + font-family: inherit; + transition: border-color 0.2s ease; + box-sizing: border-box; + height: auto; +} + +.compactInput:focus { + outline: none; + border-color: #007AFF; + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1); +} + +.compactInput::placeholder { + color: #adadad; +} + +/* Participant search styles */ +.searchContainer { + position: relative; + margin-bottom: 1rem; +} + +.searchDropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #D2D9E0; + border-top: none; + border-radius: 0 0 0.5rem 0.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + max-height: 200px; + overflow-y: auto; + z-index: 1000; +} + +.dropdownItem { + padding: 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.2s ease; +} + +.dropdownItem:hover { + background-color: #f8f9fa; +} + +.dropdownItem:last-child { + border-bottom: none; +} + +.personName { + display: block; + font-weight: 500; + color: #333; + font-size: 0.9rem; +} + +.personUsername { + display: block; + color: #666; + font-size: 0.8rem; + margin-top: 0.25rem; +} + +.participantsContainer { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + min-height: 2.5rem; + align-items: center; +} + +.participantChip { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: #E3F2FD; + color: #1976D2; + padding: 0.4rem 0.6rem; + border-radius: 1rem; + font-size: 0.85rem; + font-weight: 500; +} + +.participantName { + font-weight: 500; +} + +.removeButton { + background: none; + border: none; + color: #1976D2; + font-size: 1.2rem; + font-weight: bold; + cursor: pointer; + padding: 0; + margin-left: 0.25rem; + width: 1.5rem; + height: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.removeButton:hover { + background-color: rgba(25, 118, 210, 0.1); +} + +.removeButton:active { + background-color: rgba(25, 118, 210, 0.2); +} + +.noParticipants { + color: #999; + font-style: italic; + font-size: 0.9rem; +} + +/* Confirmation dialog styles */ +.confirmationSection { + background-color: #FFF8DC; + border: 2px solid #FFD700; + border-radius: 0.5rem; + padding: 1.5rem; + margin-top: 1rem; +} + +.confirmationMessage { + text-align: center; + margin-bottom: 1.5rem; +} + +.warningIcon { + font-size: 2rem; + display: block; + margin-bottom: 0.5rem; +} + +.confirmationMessage p { + margin: 0.5rem 0; + color: #333; +} + +.confirmationMessage p:first-of-type { + font-weight: 600; + font-size: 1.1rem; +} + +.bookingDetails { + font-size: 0.9rem; + color: #666; + font-style: italic; +} + +.confirmationButtons { + display: flex; + gap: 1rem; + justify-content: center; +} + +.confirmDeleteButton { + background-color: #DC2626; + color: white; + height: 3rem; + font-weight: 600; + font-size: 0.95rem; + border: 2px solid #B91C1C; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2); + cursor: pointer; + padding: 0 1.5rem; +} + +.confirmDeleteButton:hover { + background-color: #B91C1C; + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +.confirmDeleteButton:active { + background-color: #991B1B; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(220, 38, 38, 0.2); +} + +.cancelDeleteButton { + background-color: white; + height: 3rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.5rem; + transition: all 0.2s ease; + cursor: pointer; + font-size: 0.95rem; + padding: 0 1.5rem; +} + +.cancelDeleteButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelDeleteButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.confirmDeleteButton[data-focused], +.cancelDeleteButton[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} \ No newline at end of file diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index 516c003..7726afc 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -1,23 +1,47 @@ +import React from 'react'; import { DatePicker } from '../react-aria-starter/src/DatePicker'; import { today, getLocalTimeZone } from '@internationalized/date'; import { getFutureDate, isDateUnavailable } from '../utils/bookingUtils'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; export function BookingDatePicker() { const booking = useBookingContext(); + const { settings, getEffectiveToday } = useSettingsContext(); + const minDate = getEffectiveToday(); + const maxDate = minDate.add({ days: settings.bookingRangeDays }); + + const handlePreviousDay = () => { + const previousDay = booking.selectedDate.subtract({ days: 1 }); + if (previousDay.compare(minDate) >= 0) { + booking.handleDateChange(previousDay); + } + }; + + const handleNextDay = () => { + const nextDay = booking.selectedDate.add({ days: 1 }); + if (nextDay.compare(maxDate) <= 0) { + booking.handleDateChange(nextDay); + } + }; + + const canNavigatePrevious = booking.selectedDate.compare(minDate) > 0; + const canNavigateNext = booking.selectedDate.compare(maxDate) < 0; + return (
-

Boka rum

-
- booking.handleDateChange(date)} - firstDayOfWeek="mon" - minValue={today(getLocalTimeZone())} - maxValue={getFutureDate(14)} - isDateUnavailable={isDateUnavailable} - /> -
+ booking.handleDateChange(date)} + firstDayOfWeek="mon" + minValue={minDate} + maxValue={maxDate} + isDateUnavailable={(date) => isDateUnavailable(date, minDate, settings.bookingRangeDays)} + onPreviousClick={handlePreviousDay} + onNextClick={handleNextDay} + canNavigatePrevious={canNavigatePrevious} + canNavigateNext={canNavigateNext} + />
); } \ No newline at end of file diff --git a/my-app/src/components/BookingDetailsModal.jsx b/my-app/src/components/BookingDetailsModal.jsx new file mode 100644 index 0000000..c2c68a0 --- /dev/null +++ b/my-app/src/components/BookingDetailsModal.jsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; +import Dropdown from './Dropdown'; +import styles from './BookingDetailsModal.module.css'; + +function BookingDetailsModal({ booking, isOpen, onClose, onSave }) { + const [selectedLength, setSelectedLength] = useState(null); + const [calculatedEndTime, setCalculatedEndTime] = useState(null); + + // Calculate current booking length and available hours + const currentLength = booking ? booking.endTime - booking.startTime : 1; + // For simplicity, assume max booking time is 8 hours (16 half-hour slots) + const maxAvailableTime = 16; // This could be dynamic based on room availability + const hoursAvailable = booking ? Math.min(maxAvailableTime - booking.startTime, 8) : 8; + + // Initialize state when modal opens or booking changes + useEffect(() => { + if (isOpen && booking) { + setSelectedLength(currentLength); + setCalculatedEndTime(booking.endTime); + } + }, [isOpen, booking, currentLength]); + + if (!booking) return null; + + 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 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), + }; + + function handleLengthChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = booking.startTime + lengthValue; + setCalculatedEndTime(newEndTime); + } else { + setCalculatedEndTime(booking.endTime); + } + } + + function handleSave() { + if (selectedLength !== null && onSave) { + const updatedBooking = { + ...booking, + endTime: calculatedEndTime + }; + onSave(updatedBooking); + } + onClose(); + } + + function handleCancel() { + setSelectedLength(currentLength); + setCalculatedEndTime(booking.endTime); + onClose(); + } + + function formatParticipants(participants) { + if (!participants || participants.length === 0) return 'Inga deltagare'; + + if (participants.length === 1) { + return participants[0].name; + } else if (participants.length === 2) { + return `${participants[0].name} and ${participants[1].name}`; + } else { + const remaining = participants.length - 2; + return `${participants[0].name}, ${participants[1].name} and ${remaining} more`; + } + } + + return ( + + +
+ + {booking.title} + + +
+ +
+
+ +

{convertDateObjectToString(booking.date)}

+
+ +
+
+
+ + + {getTimeFromIndex(booking.startTime)} + +
+
+
+ + + {getTimeFromIndex(calculatedEndTime || booking.endTime)} + +
+
+
+ +
+ + +
+ +
+ +

{booking.room}

+
+ +
+ +

{formatParticipants(booking.participants)}

+
+
+ +
+ + +
+
+
+ ); +} + +export default BookingDetailsModal; \ No newline at end of file diff --git a/my-app/src/components/BookingDetailsModal.module.css b/my-app/src/components/BookingDetailsModal.module.css new file mode 100644 index 0000000..e1f2ec5 --- /dev/null +++ b/my-app/src/components/BookingDetailsModal.module.css @@ -0,0 +1,244 @@ +.modalContainer { + background-color: transparent; + width: 85%; + max-width: 400px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.18) !important; + box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), + 0 16px 32px rgba(0, 0, 0, 0.08), + 0 8px 16px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; + backdrop-filter: blur(20px) saturate(140%) !important; + -webkit-backdrop-filter: blur(20px) saturate(140%) !important; + border-radius: 0.4rem; +} + +.dialog { + overflow: hidden; + background: rgba(255, 255, 255, 0.95) !important; + padding: 0; + border-radius: 0.4rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 1.5rem 0; + margin-bottom: 1rem; +} + +.title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + flex: 1; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + color: #6b7280; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + margin-left: 1rem; +} + +.closeButton:hover { + background-color: #f3f4f6; + color: #374151; +} + +.closeButton:focus { + outline: 2px solid #2563eb; + outline-offset: -1px; +} + +.content { + padding: 0 1.5rem 1.5rem; +} + +.sectionWithTitle { + padding-bottom: 1rem; + display: flex; + flex-direction: column; +} + +.sectionWithTitle label { + font-size: 0.8rem; + color: #717171; + font-weight: 500; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.sectionWithTitle p { + margin: 0; + font-size: 1rem; + color: #1f2937; + font-weight: 500; +} + +.timeDisplay { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + width: fit-content; + margin-left: 0; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.startTime, .endTime { + display: flex; + flex-direction: column; + align-items: center; +} + +.startTime label, .endTime label { + font-size: 0.75rem; + color: #6c757d; + font-weight: 500; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.timeValue { + font-size: 1.5rem; + font-weight: 600; + color: #212529; +} + +.timeSeparator { + font-size: 1.5rem; + font-weight: 400; + color: #6c757d; + margin: 0 0.5rem; + padding-top: 1.3rem; +} + +.roomText { + font-weight: 600 !important; + color: #059669 !important; +} + +.modalFooter { + height: fit-content; + width: 100%; + display: flex; + align-items: center; + gap: 1rem; + margin-top: 2rem; + padding: 0 1.5rem 1.5rem; +} + +.cancelButton { + flex: 2; + background-color: white; + height: 4rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.5rem; + transition: all 0.2s ease; + cursor: pointer; +} + +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.saveButton { + flex: 3; + background-color: #059669; + color: white; + height: 4rem; + font-weight: 600; + font-size: 1.1rem; + border: 2px solid #047857; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); + cursor: pointer; +} + +.saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); +} + +.saveButton:active { + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); +} + +.saveButton[data-focused], +.cancelButton[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} + +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + opacity: 0.6 !important; + box-shadow: none !important; + cursor: default !important; +} + +.disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none !important; +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; +} + +/* Modal overlay styles */ +:global(.react-aria-ModalOverlay) { + z-index: 1100 !important; + overflow-y: auto !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + padding: 2rem 1rem !important; + box-sizing: border-box !important; + background: rgba(0, 0, 0, 0.2) !important; + backdrop-filter: blur(12px) saturate(150%) !important; + -webkit-backdrop-filter: blur(12px) saturate(150%) !important; +} + +:global(.react-aria-ModalOverlay .react-aria-Modal) { + max-height: calc(100vh - 4rem) !important; + max-width: 90vw !important; + overflow-y: auto !important; +} \ No newline at end of file diff --git a/my-app/src/components/BookingFormFields.jsx b/my-app/src/components/BookingFormFields.jsx deleted file mode 100644 index 1f5e6de..0000000 --- a/my-app/src/components/BookingFormFields.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import Dropdown from './Dropdown'; -import { ComboBox } from '../react-aria-starter/src/ComboBox'; -import { DEFAULT_BOOKING_TITLE, BOOKING_LENGTHS, SMALL_GROUP_ROOMS, PEOPLE } from '../constants/bookingConstants'; -import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; - -export function BookingFormFields() { - const booking = useBookingContext(); - return ( - <> -

Titel på bokning

- booking.setTitle(event.target.value)} - placeholder={DEFAULT_BOOKING_TITLE} - className={styles.textInput} - /> - -

Deltagare

- - - {booking.participants.length > 0 && ( -
- {booking.participants.map((participant, index) => ( -

{participant}

- ))} -
- )} - -
-
-

Rum

- booking.handleRoomChange(e)} - placeholder={{ - label: "Alla rum", - value: "allRooms" - }} - /> -
-
-

Längd

- booking.handleLengthChange(Number(e.target.value))} - placeholder={{ - label: "Alla tider", - value: 0 - }} - disabledOptions={booking.disabledOptions} - /> -
-
- - ); -} \ No newline at end of file diff --git a/my-app/src/components/BookingFormFields.module.css b/my-app/src/components/BookingFormFields.module.css deleted file mode 100644 index c3ec05b..0000000 --- a/my-app/src/components/BookingFormFields.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.textInput { - width: 100%; - margin-bottom: 10px; - border: 1px solid #D2D9E0; - border-radius: 0.5rem; - font-size: 16px; - background-color: #FAFBFC; - padding: 1rem; - width: 300px; - font-family: inherit; -} - -.textInput::placeholder { - color: #adadad; -} - -.elementHeading { - margin: 0; - color: #8E8E8E; - font-size: 0.8rem; - font-style: normal; - font-weight: 520; - line-height: normal; - margin-bottom: 0.2rem; - margin-top: 1.5rem; -} \ No newline at end of file diff --git a/my-app/src/components/BookingLengthField.jsx b/my-app/src/components/BookingLengthField.jsx new file mode 100644 index 0000000..5af8eac --- /dev/null +++ b/my-app/src/components/BookingLengthField.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Dropdown from './Dropdown'; +import { BOOKING_LENGTHS } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './BookingLengthField.module.css'; + +export function BookingLengthField() { + const booking = useBookingContext(); + + return ( +
+

Längd

+ booking.handleLengthChange(Number(e.target.value))} + placeholder={{ + label: "Alla tider", + value: 0 + }} + disabledOptions={booking.disabledOptions} + /> +
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/BookingLengthField.module.css b/my-app/src/components/BookingLengthField.module.css new file mode 100644 index 0000000..c5ad2cc --- /dev/null +++ b/my-app/src/components/BookingLengthField.module.css @@ -0,0 +1,9 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; +} \ No newline at end of file diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx new file mode 100644 index 0000000..0a4945f --- /dev/null +++ b/my-app/src/components/BookingModal.jsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; +import Dropdown from './Dropdown'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './BookingModal.module.css'; + +export function BookingModal({ + startTimeIndex, + hoursAvailable, + endTimeIndex, + setEndTimeIndex, + className, + onClose, + isOpen = true +}) { + const booking = useBookingContext(); + const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext(); + + // Initialize with pre-selected booking length if available, or auto-select if only 30 min available + const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : + (hoursAvailable === 1 ? 1 : null); // Auto-select 30 min if that's all that's available + const [selectedLength, setSelectedLength] = useState(null); + const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex); + const hasInitialized = useRef(false); + + // Effect to handle initial setup only once when modal opens + useEffect(() => { + if (initialLength && !hasInitialized.current) { + setSelectedLength(initialLength); + const newEndTime = startTimeIndex + initialLength; + setCalculatedEndTime(newEndTime); + setEndTimeIndex(newEndTime); + booking.setSelectedEndIndex(newEndTime); + hasInitialized.current = true; + } + }, [initialLength, startTimeIndex, setEndTimeIndex, booking]); + + 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" }, + ]; + + function getLabelFromAvailableHours(availableHours) { + return bookingLengths.find(option => option.value === availableHours)?.label || "Välj längd"; + } + + 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), + }; + + function handleChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); + console.log(event.target.value); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = startTimeIndex + lengthValue; + setCalculatedEndTime(newEndTime); + setEndTimeIndex(newEndTime); + booking.setSelectedEndIndex(newEndTime); + } else { + // Reset to default state when placeholder is selected + setCalculatedEndTime(startTimeIndex); + setEndTimeIndex(startTimeIndex); + booking.setSelectedEndIndex(null); + } + } + + // Check if user has selected a booking length (including pre-selected) + const hasSelectedLength = selectedLength !== null; + + // Display time range - show calculated end time if length is selected + const displayEndTime = hasSelectedLength ? calculatedEndTime : startTimeIndex; + + + return ( + !open && onClose && onClose()} + className={className} + style={{borderRadius: '0.4rem', overflow: 'hidden'}} + > + +
+ {booking.title == "" ? getDefaultBookingTitle() : booking.title} +

{convertDateObjectToString(booking.selectedDate)}

+
+
+
+ + {getTimeFromIndex(startTimeIndex)} +
+
+
+ + + {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} + +
+
+
+ +
+ + +
+ +
+ +

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

+
+ +
+ +

+ {(() => { + const currentUser = getCurrentUser(); + const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)]; + return allParticipants.map(p => p.name).join(", "); + })()} +

+
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/BookingModal.module.css b/my-app/src/components/BookingModal.module.css new file mode 100644 index 0000000..285d7a3 --- /dev/null +++ b/my-app/src/components/BookingModal.module.css @@ -0,0 +1,216 @@ +/* TIME CARD */ + +.startTime { + width: fit-content; + font-weight: 600; + font-size: 1.3rem; +} + +.modalFooter { + height: fit-content; + width: 100%; + color: blue; + display: flex; + align-items: center; + gap: 1rem; + margin-top: 2rem; +} + + +/* MODAL */ + +.cancelButton { + flex: 2; + background-color: white; + height: 4rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.5rem; + transition: all 0.2s ease; +} + +@media (hover: hover) { + .cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; + } +} + +.saveButton { + flex: 3; + background-color: #059669; + color: white; + height: 4rem; + font-weight: 600; + font-size: 1.1rem; + border: 2px solid #047857; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); +} + +@media (hover: hover) { + .saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; + } +} + +.saveButton:active { + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); +} + +.saveButton[data-focused], +.cancelButton[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.timeSpan { + font-size: 2rem; + margin: 0; +} + +.sectionWithTitle { + padding-top: 1rem; + display: flex; + flex-direction: column; + width: fit-content +} + +.sectionWithTitle label { + font-size: 0.8rem; + color: #717171; +} + +.sectionWithTitle p { + margin: 0; +} + +.modalContainer { + background-color: white; + width: 85%; + max-width: 400px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.18) !important; + box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), + 0 16px 32px rgba(0, 0, 0, 0.08), + 0 8px 16px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; + backdrop-filter: blur(20px) saturate(140%) !important; + -webkit-backdrop-filter: blur(20px) saturate(140%) !important; +} + +/* Ensure modal appears above header and handles overflow */ +:global(.react-aria-ModalOverlay) { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + z-index: 1100 !important; + overflow-y: auto !important; + display: flex !important; + align-items: safe center !important; + justify-content: center !important; + padding: 2rem 1rem !important; + box-sizing: border-box !important; + background: rgba(0, 0, 0, 0.5) !important; + backdrop-filter: blur(12px) saturate(150%) !important; + -webkit-backdrop-filter: blur(12px) saturate(150%) !important; +} + +:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal) { + max-height: calc(100vh - 4rem) !important; + max-width: 90vw !important; + overflow-y: auto !important; + background: white !important; + border-radius: 0.5rem !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important; +} + +/* New time display styles */ +.timeDisplay { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + min-width: 196px; + width: fit-content; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.startTime, .endTime { + display: flex; + flex-direction: column; + align-items: center; +} + +.startTime label, .endTime label { + font-size: 0.75rem; + color: #6c757d; + font-weight: 500; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.timeValue { + font-size: 1.5rem; + font-weight: 600; + color: #212529; +} + +.timeValue.placeholder { + color: #adb5bd; + font-style: italic; + font-size: 1rem; +} + +.timeSeparator { + font-size: 1.5rem; + font-weight: 400; + color: #6c757d; + margin: 0 0.5rem; + padding-top: 1.3rem; +} + +/* Disabled button styles */ +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + opacity: 0.6 !important; + box-shadow: none; +} + +@media (hover: hover) { + .disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none; + cursor: default; + } +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; +} \ No newline at end of file diff --git a/my-app/src/components/BookingTitleField.jsx b/my-app/src/components/BookingTitleField.jsx new file mode 100644 index 0000000..71eca53 --- /dev/null +++ b/my-app/src/components/BookingTitleField.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './BookingTitleField.module.css'; + +export function BookingTitleField({ compact = false }) { + const booking = useBookingContext(); + const { getDefaultBookingTitle } = useSettingsContext(); + + return ( + <> +

Titel på bokning

+ booking.setTitle(event.target.value)} + placeholder={getDefaultBookingTitle()} + className={compact ? styles.compactTextInput : styles.textInput} + /> + + ); +} \ No newline at end of file diff --git a/my-app/src/components/BookingTitleField.module.css b/my-app/src/components/BookingTitleField.module.css new file mode 100644 index 0000000..fb75a3b --- /dev/null +++ b/my-app/src/components/BookingTitleField.module.css @@ -0,0 +1,57 @@ +.textInput { + width: 100%; + margin-bottom: 10px; + border: 1px solid #D2D9E0; + border-radius: 0.5rem; + font-size: 16px; + background-color: #FAFBFC; + padding: 1rem; + width: 100%; + max-width: 600px; + font-family: inherit; +} + +.textInput::placeholder { + color: #adadad; +} + +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} + +/* Compact styles */ +.compactElementHeading { + font-size: 0.75rem; + color: #717171; + font-weight: 500; + margin-bottom: 0.4rem; + margin-top: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.compactTextInput { + width: 100%; + padding: 0.5rem 1rem; + border: 1px solid #ccc; + border-radius: 0.375rem; + font-size: 16px; + background-color: white; + font-family: inherit; + transition: border-color 0.2s ease; + box-sizing: border-box; + margin-bottom: 0; +} + +.compactTextInput:focus { + outline: 2px solid #007AFF; + outline-offset: 2px; + border-color: #007AFF; +} \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index b489bb2..91cc60e 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -1,17 +1,103 @@ +import React, { useState } from 'react'; +import { CalendarDate } from '@internationalized/date'; import styles from './BookingsList.module.css'; -import Booking from './Booking'; +import BookingCard from './BookingCard'; +import NotificationBanner from './NotificationBanner'; -function BookingsList({ bookings, handleEditBooking }) { +function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) { + const [showAll, setShowAll] = useState(false); + const [expandedBookingId, setExpandedBookingId] = useState(null); + const INITIAL_DISPLAY_COUNT = 3; + + const displayedBookings = showAll ? bookings : bookings.slice(0, INITIAL_DISPLAY_COUNT); + const hasMoreBookings = bookings.length > INITIAL_DISPLAY_COUNT; + + function handleBookingClick(booking) { + setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id); + } - return (
+ {showSuccessBanner && ( + + )} + {showDeleteBanner && ( + + )} + {showDevelopmentBanner && ( + + )} + {showBookingConfirmationBanner && ( + + )} + {showBookingDeleteBanner && ( + + )}
{bookings.length > 0 ? ( <> - {bookings.map((booking, index) => ( - + {displayedBookings.map((booking, index) => ( + handleBookingClick(booking)} + isExpanded={expandedBookingId === booking.id} + onBookingUpdate={onBookingUpdate} + onBookingDelete={onBookingDelete} + /> ))} + {hasMoreBookings && ( + + )} ) : (

Du har inga bokningar just nu

diff --git a/my-app/src/components/BookingsList.module.css b/my-app/src/components/BookingsList.module.css index 2bd4ecd..de5ba1a 100644 --- a/my-app/src/components/BookingsList.module.css +++ b/my-app/src/components/BookingsList.module.css @@ -1,10 +1,8 @@ .bookingsListContainer { - padding: 1rem; /*border-top: 1px solid gray;*/ padding-bottom: 2rem; display: flex; flex-direction: column; - padding-top: 1rem; } .bookingsContainer { @@ -22,4 +20,48 @@ .heading { margin: 0; margin-bottom: 0.5rem; +} + +.showMoreButton { + background: #ffffff; + border: none; + border-radius: 0.75rem; + padding: 1rem; + font-size: 1rem; + font-weight: 600; + color: rgb(0, 0, 0); + cursor: pointer; + transition: all 0.2s ease; + margin-top: 1rem; + width: 100%; + max-width: 100%; + box-shadow: 0 2px 8px rgba(143, 143, 143, 0.2); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + box-sizing: border-box; +} + +.showMoreButton:hover { + background: #f2f6ff; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(192, 192, 192, 0.3); +} + +.showMoreButton:active { + transform: translateY(0); +} + + + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/my-app/src/components/Card.jsx b/my-app/src/components/Card.jsx index 940676e..7eca5cc 100644 --- a/my-app/src/components/Card.jsx +++ b/my-app/src/components/Card.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './Card.module.css'; // Import the CSS Module const Card = ({ imageUrl, header, subheader }) => { diff --git a/my-app/src/components/ComboBox.jsx b/my-app/src/components/ComboBox.jsx new file mode 100644 index 0000000..1c88c62 --- /dev/null +++ b/my-app/src/components/ComboBox.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { + Button, + ComboBox as AriaComboBox, + FieldError, + Input, + Label, + ListBox, + ListBoxItem, + Popover, + Text, + ListBoxSection, + Header, + Collection +} from 'react-aria-components'; +import styles from './ComboBox.module.css'; + +export function ComboBox({ + label, + description, + errorMessage, + children, + items, + ...props +}) { + return ( + + +
+ + +
+ {description && {description}} + {errorMessage} + + + +
SENASTE SÖKNINGAR
+ + {item => {item.name}} + +
+
+
+
+ ); +} + +export function ComboBoxItem(props) { + return ; +} + +function UserItem({ children, ...props }) { + return ( + + {({ isSelected }) => ( + <> + + {children} + + {isSelected && ( + + ✓ + + )} + + )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/ComboBox.module.css b/my-app/src/components/ComboBox.module.css new file mode 100644 index 0000000..e0a55be --- /dev/null +++ b/my-app/src/components/ComboBox.module.css @@ -0,0 +1,150 @@ +.comboBoxLabel { + color: var(--text-color); +} + +.comboBoxContainer { + display: flex; + align-items: center; + font-family: inherit; +} + +.comboBoxInput { + margin: 0; + font-size: 16px; + background-color: var(--field-background); + color: var(--field-text-color); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + vertical-align: middle; + outline: none; + min-width: 0; + font-family: inherit; + width: 100%; + max-width: 600px; +} + +.comboBoxInput[data-focused] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; +} + +.comboBoxButton { + background: var(--highlight-background); + color: var(--highlight-foreground); + forced-color-adjust: none; + border-radius: 4px; + border: none; + margin-left: -1.714rem; + width: 1.429rem; + height: 1.429rem; + padding: 0; + font-size: 0.857rem; + cursor: default; + flex-shrink: 0; +} + +.comboBoxButton[data-pressed] { + box-shadow: none; + background: var(--highlight-background); +} + +.comboBoxDescription { + font-size: 12px; +} + +.comboBoxError { + font-size: 12px; + color: var(--invalid-color); +} + +.comboBoxPopover[data-trigger="ComboBox"] { + width: var(--trigger-width); +} + +.comboBoxPopover { + box-sizing: border-box; + background-color: white; +} + +.comboBoxList { + display: block; + margin: 0; + padding: 0; + width: unset; + max-height: inherit; + min-height: unset; + border: none; + background-color: white; +} + +.userItem { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: default; + outline: none; + border-radius: 0.125rem; + color: var(--text-color); + padding: 0.5rem; +} + +.userItem[data-focused], +.userItem[data-pressed] { + background: var(--highlight-background); + color: var(--highlight-foreground); +} + +.userItem[data-selected] { + font-weight: 700; + background: unset; + color: var(--text-color); + position: relative; +} + +.userItemContent { + flex: 1; + display: flex; + align-items: center; + gap: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: normal; +} + +.userItem[data-selected] .userItemContent { + font-weight: medium; +} + +.userItemCheckIcon { + width: 1.25rem; + display: flex; + align-items: center; + color: var(--highlight-background); +} + +.userItem[data-focused] .userItemCheckIcon { + color: var(--highlight-foreground); +} + +.comboBoxAvatar { + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: lightgray; +} + +.comboBoxName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sectionHeader { + font-weight: 700; + color: rgb(89, 89, 89); + font-size: 0.9rem; + margin-bottom: 0.4rem; + padding: 0 0.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/Dropdown.jsx b/my-app/src/components/Dropdown.jsx index 6bc5dd1..5807b2f 100644 --- a/my-app/src/components/Dropdown.jsx +++ b/my-app/src/components/Dropdown.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from "./Dropdown.module.css"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' diff --git a/my-app/src/components/Dropdown.module.css b/my-app/src/components/Dropdown.module.css index d681482..4b44050 100644 --- a/my-app/src/components/Dropdown.module.css +++ b/my-app/src/components/Dropdown.module.css @@ -1,29 +1,33 @@ .dropdownWrapper { position: relative; display: inline-block; + width: 100%; + max-width: 200px; } .select { font-family: inherit; appearance: none; -webkit-appearance: none; - padding: 0.5rem 2rem 0.5rem 1rem; /* Make room on right for chevron */ + padding: 0.5rem 2.5rem 0.5rem 1rem; /* More room on right for chevron */ border: 1px solid #ccc; border-radius: 0.375rem; background-color: white; color: #333; cursor: pointer; font-size: 1rem; - width: fit-content; - min-width: 6rem; /* Optional: prevent it from getting too small */ + width: 100%; + min-width: 150px; + box-sizing: border-box; } .chevron { pointer-events: none; position: absolute; top: 50%; - right: 0.75rem; + right: 1rem; transform: translateY(-50%); color: #888; font-size: 0.8rem; + z-index: 1; } diff --git a/my-app/src/components/Header.jsx b/my-app/src/components/Header.jsx index 5c00ecb..b96f4e0 100644 --- a/my-app/src/components/Header.jsx +++ b/my-app/src/components/Header.jsx @@ -31,6 +31,7 @@ const Header = () => {
{/* Menu items */} Lokalbokning + Booking Settings
)} diff --git a/my-app/src/components/InlineBookingForm.jsx b/my-app/src/components/InlineBookingForm.jsx new file mode 100644 index 0000000..7a3396a --- /dev/null +++ b/my-app/src/components/InlineBookingForm.jsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button } from 'react-aria-components'; +import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; +import Dropdown from './Dropdown'; +import { BookingTitleField } from './BookingTitleField'; +import { ParticipantsSelector } from './ParticipantsSelector'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './InlineBookingForm.module.css'; + +export function InlineBookingForm({ + startTimeIndex, + hoursAvailable, + onClose, + arrowPointsLeft = true +}) { + const booking = useBookingContext(); + const { getCurrentUser } = useSettingsContext(); + + // Initialize with pre-selected booking length if available, or auto-select if only 30 min available + const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : + (hoursAvailable === 1 ? 1 : null); // Auto-select 30 min if that's all that's available + const [selectedLength, setSelectedLength] = useState(null); + const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex); + const hasInitialized = useRef(false); + + // Effect to handle initial setup only once when form opens + useEffect(() => { + if (initialLength && !hasInitialized.current) { + setSelectedLength(initialLength); + const newEndTime = startTimeIndex + initialLength; + setCalculatedEndTime(newEndTime); + booking.setSelectedEndIndex(newEndTime); + hasInitialized.current = true; + } + }, [initialLength, startTimeIndex, booking]); + + 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 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), + }; + + function handleChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = startTimeIndex + lengthValue; + setCalculatedEndTime(newEndTime); + booking.setSelectedEndIndex(newEndTime); + } else { + // Reset to default state when placeholder is selected + setCalculatedEndTime(startTimeIndex); + booking.setSelectedEndIndex(null); + } + } + + // Check if user has selected a booking length (including pre-selected) + const hasSelectedLength = selectedLength !== null; + + // Display time range - show calculated end time if length is selected + const displayEndTime = hasSelectedLength ? calculatedEndTime : startTimeIndex; + + return ( +
+ {/* Date Context */} +
+

{convertDateObjectToString(booking.selectedDate)}

+
+ + {/* Title - What */} +
+ +
+ + {/* Time Selection - When */} +
+
+
+
+ + {getTimeFromIndex(startTimeIndex)} +
+
+
+ + + {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} + +
+
+
+ +
+ + +
+
+ + {/* Participants - Who */} +
+ +
+ + {/* Room - Where */} +
+
+ +

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

+
+
+ + {/* Actions */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/InlineBookingForm.module.css b/my-app/src/components/InlineBookingForm.module.css new file mode 100644 index 0000000..1f78cc8 --- /dev/null +++ b/my-app/src/components/InlineBookingForm.module.css @@ -0,0 +1,252 @@ +.inlineForm { + background: white; + border: 1px solid #D1D5DB; + border-radius: 0.5rem; + padding: 1.5rem; + margin: 1rem 0; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + animation: slideDown 0.2s ease-out; + width: 100%; + flex-basis: 100%; + max-width: none; + position: relative; +} + +/* Arrow pointing to left card */ +.arrowLeft::before { + content: ''; + position: absolute; + top: -8px; + left: 75px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #D1D5DB; +} + +.arrowLeft::after { + content: ''; + position: absolute; + top: -7px; + left: 76px; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid white; +} + +/* Arrow pointing to right card */ +.arrowRight::before { + content: ''; + position: absolute; + top: -8px; + right: 75px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #D1D5DB; +} + +.arrowRight::after { + content: ''; + position: absolute; + top: -7px; + right: 76px; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid white; +} + +.formHeader { + text-align: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #E5E7EB; +} + +.section { + margin-bottom: 1.5rem; +} + +.section:last-of-type { + margin-bottom: 0; +} + +.formHeader h3 { + margin: 0 0 0.5rem 0; + font-size: 1.25rem; + font-weight: 700; + color: #111827; +} + +.dateText { + margin: 0; + color: #6B7280; + font-size: 0.875rem; +} + +.timeDisplay { + margin-bottom: 1rem; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 1rem; + background: #F9FAFB; + border-radius: 0.375rem; +} + +.timeItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.timeItem label { + font-size: 0.75rem; + color: #6B7280; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.timeValue { + font-size: 1.25rem; + font-weight: 600; + color: #111827; +} + +.timeValue.placeholder { + color: #9CA3AF; + font-style: italic; +} + +.timeSeparator { + font-size: 1.5rem; + color: #6B7280; + font-weight: 300; +} + +.formField { + margin-bottom: 1rem; +} + +.formField label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.5rem; +} + +.sectionWithTitle { + padding-top: 1rem; + display: flex; + flex-direction: column; + width: fit-content; +} + +.sectionWithTitle label { + font-size: 0.8rem; + color: #717171; +} + +.sectionWithTitle p { + margin: 0; +} + +.formActions { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid #E5E7EB; +} + +.cancelButton { + flex: 1; + background-color: white; + height: 2.75rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.375rem; + transition: all 0.2s ease; + cursor: pointer; + font-size: 0.875rem; +} + +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.saveButton { + flex: 2; + background-color: #059669; + color: white; + height: 2.75rem; + font-weight: 600; + font-size: 0.875rem; + border: 2px solid #047857; + border-radius: 0.375rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); + cursor: pointer; +} + +.saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); +} + +.saveButton:active { + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); +} + +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + opacity: 0.6 !important; + box-shadow: none !important; + cursor: default !important; +} + +.disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none !important; +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/my-app/src/components/NotificationBanner.jsx b/my-app/src/components/NotificationBanner.jsx new file mode 100644 index 0000000..37e4108 --- /dev/null +++ b/my-app/src/components/NotificationBanner.jsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import styles from './NotificationBanner.module.css'; +import { convertDateObjectToString } from '../helpers'; + +const BANNER_VARIANTS = { + success: { + icon: '✓', + title: 'Bokning bekräftad:', + className: 'success' + }, + delete: { + icon: '🗑️', + title: 'Bokning raderad:', + className: 'delete' + }, + development: { + icon: '🔧', + title: 'Visar testdata för utveckling', + className: 'development' + } +}; + +function NotificationBanner({ + variant = 'success', + booking = null, + onClose, + showCloseButton = false, + showFakeCloseButton = false, + isTestBanner = false, + customTitle = null, + customContent = null +}) { + const [showTooltip, setShowTooltip] = useState(false); + + 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 formatBookingDetails(booking) { + if (!booking) return ''; + + const dateStr = convertDateObjectToString(booking.date); + const startTime = getTimeFromIndex(booking.startTime); + const endTime = getTimeFromIndex(booking.endTime); + + return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`; + } + + const handleFakeClose = () => { + setShowTooltip(true); + setTimeout(() => setShowTooltip(false), 3000); + }; + + const config = BANNER_VARIANTS[variant] || BANNER_VARIANTS.success; + + // For development banner, use custom content + if (variant === 'development') { + return ( +
+
+ {config.icon} + {customTitle || config.title} +
+
+ ); + } + + // For booking-related banners (success/delete) + if (!booking && !customContent) return null; + + return ( +
+
+ + {config.icon} + +
+
+ + {customTitle || config.title} {booking && booking.title && {booking.title}} + + {isTestBanner && TEST} +
+ {booking && ( + + {formatBookingDetails(booking)} + + )} + {customContent && ( + + {customContent} + + )} +
+
+ {showCloseButton && ( + + )} + {showFakeCloseButton && ( +
+ + {showTooltip && ( +
+ Detta är en testbanner som inte kan stängas +
+ )} +
+ )} +
+ ); +} + +export default NotificationBanner; \ No newline at end of file diff --git a/my-app/src/components/NotificationBanner.module.css b/my-app/src/components/NotificationBanner.module.css new file mode 100644 index 0000000..c60b351 --- /dev/null +++ b/my-app/src/components/NotificationBanner.module.css @@ -0,0 +1,177 @@ +/* Base banner styles */ +.banner { + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.bannerContent { + display: flex; + align-items: center; + gap: 1rem; +} + +.icon { + width: 2rem; + height: 2rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1rem; + flex-shrink: 0; +} + +.text { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.titleRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.title { + font-weight: 700; + font-size: 1.1rem; +} + +.details { + font-weight: 500; + font-size: 0.9rem; +} + +.bookingTitle { + font-weight: 400; +} + +.closeButton { + background: none; + border: none; + color: #6C757D; + font-size: 1.5rem; + font-weight: 300; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + transition: all 0.2s ease; + line-height: 1; + flex-shrink: 0; + width: fit-content; +} + +.closeButton:hover { + background: rgba(108, 117, 125, 0.1); + color: #495057; +} + +/* Success variant styles */ +.success { + background: #E8F5E8; + border: 1px solid #4CAF50; +} + +.successIcon { + background: #4CAF50; + color: white; +} + +.successTitle { + color: #2E7D32; +} + +.successDetails { + color: #388E3C; +} + +/* Delete variant styles */ +.delete { + background: #FFF4F4; + border: 1px solid #F87171; +} + +.deleteIcon { + background: #EF4444; + color: white; +} + +.deleteTitle { + color: #DC2626; +} + +.deleteDetails { + color: #EF4444; +} + +/* Development variant styles */ +.development { + background: #FFF8E1; + border: 1px solid #FFB74D; +} + +.developmentIcon { + font-size: 1.5rem; + color: #FF9800; +} + +/* Test label styles */ +.testLabel { + background: #FF9800; + color: white; + font-size: 0.7rem; + font-weight: 700; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Fake close and tooltip styles */ +.fakeCloseContainer { + position: relative; +} + +.tooltip { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.5rem; + background: #333; + color: white; + padding: 0.75rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + white-space: nowrap; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + animation: fadeIn 0.2s ease-out; +} + +.tooltip::before { + content: ''; + position: absolute; + bottom: 100%; + right: 1rem; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #333; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsField.jsx b/my-app/src/components/ParticipantsField.jsx new file mode 100644 index 0000000..4cebd27 --- /dev/null +++ b/my-app/src/components/ParticipantsField.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ComboBox } from './ComboBox'; +import { PEOPLE } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './ParticipantsField.module.css'; + +export function ParticipantsField() { + const booking = useBookingContext(); + + return ( + <> +

Deltagare

+ + + {booking.participants.length > 0 && ( +
+ {booking.participants.map((participant, index) => ( +

{participant}

+ ))} +
+ )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsField.module.css b/my-app/src/components/ParticipantsField.module.css new file mode 100644 index 0000000..f804eca --- /dev/null +++ b/my-app/src/components/ParticipantsField.module.css @@ -0,0 +1,10 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx new file mode 100644 index 0000000..d3f1d9d --- /dev/null +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -0,0 +1,291 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { PEOPLE, USER } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './ParticipantsSelector.module.css'; + +export function ParticipantsSelector({ compact = false }) { + const booking = useBookingContext(); + const { getCurrentUser } = useSettingsContext(); + const [searchTerm, setSearchTerm] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); + const [recentSearches, setRecentSearches] = useState([ + { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, + { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' } + ]); + const inputRef = useRef(null); + const dropdownRef = useRef(null); + const itemRefs = useRef([]); + + // Filter people based on search term + const filteredPeople = PEOPLE.filter(person => + person.name.toLowerCase().includes(searchTerm.toLowerCase()) || + person.username.toLowerCase().includes(searchTerm.toLowerCase()) || + person.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Show recent searches only when input is empty (first click) + const showRecentSearches = searchTerm === '' && recentSearches.length > 0; + // Only show all people when user starts typing + const showAllPeople = searchTerm !== ''; + const displayPeople = filteredPeople; + + // Helper function to check if person is already selected + const isPersonSelected = (personName) => booking.participants.find(p => p.name === personName); + + // Get all available options for keyboard navigation + const allOptions = showRecentSearches ? recentSearches : (showAllPeople ? displayPeople : []); + + // Helper function to get person's initials + const getInitials = (name) => { + return name.split(' ').map(word => word[0]).join('').toUpperCase(); + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Scroll focused item into view + useEffect(() => { + if (focusedIndex >= 0 && itemRefs.current[focusedIndex]) { + itemRefs.current[focusedIndex].scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + } + }, [focusedIndex]); + + const handleInputFocus = () => { + // Don't auto-open dropdown on focus - wait for user interaction + setFocusedIndex(-1); + }; + + const handleInputClick = () => { + setIsDropdownOpen(true); + setFocusedIndex(-1); + // Clear refs when dropdown opens + itemRefs.current = []; + }; + + const handleInputChange = (e) => { + setSearchTerm(e.target.value); + setIsDropdownOpen(true); + setFocusedIndex(-1); + // Clear refs when content changes + itemRefs.current = []; + }; + + const handleKeyDown = (e) => { + if (!isDropdownOpen) { + // When dropdown is closed, Enter should open it + if (e.key === 'Enter') { + e.preventDefault(); + setIsDropdownOpen(true); + setFocusedIndex(-1); + } + return; + } + + switch (e.key) { + case 'ArrowDown': + case 'Tab': + e.preventDefault(); + setFocusedIndex(prev => + prev < allOptions.length - 1 ? prev + 1 : 0 + ); + break; + case 'ArrowUp': + e.preventDefault(); + setFocusedIndex(prev => + prev > 0 ? prev - 1 : allOptions.length - 1 + ); + break; + case 'Enter': + e.preventDefault(); + if (focusedIndex >= 0 && focusedIndex < allOptions.length) { + handleSelectPerson(allOptions[focusedIndex]); + } + break; + case 'Escape': + e.preventDefault(); + setIsDropdownOpen(false); + setFocusedIndex(-1); + // Keep input focused with outline, don't blur completely + inputRef.current?.focus(); + break; + } + }; + + const handleSelectPerson = (person) => { + console.log('handleSelectPerson called with:', person); + + // Don't add if already selected + if (isPersonSelected(person.name)) { + setSearchTerm(''); + setIsDropdownOpen(false); + setFocusedIndex(-1); + // Keep focus on input instead of blurring + inputRef.current?.focus(); + return; + } + + booking.handleParticipantChange(person.id); + + // Add to recent searches if not already there + if (!recentSearches.find(p => p.id === person.id)) { + setRecentSearches(prev => [person, ...prev.slice(0, 4)]); + } + + setSearchTerm(''); + setIsDropdownOpen(false); + setFocusedIndex(-1); + // Keep focus on input for continued interaction + inputRef.current?.focus(); + }; + + const handleRemoveParticipant = (participantToRemove) => { + booking.handleRemoveParticipant(participantToRemove); + }; + + return ( +
+

Deltagare

+ + {/* Search Input */} +
+ = 0 ? `option-${focusedIndex}` : undefined} + /> + + {/* Dropdown */} + {isDropdownOpen && ( +
+ {/* Recent Searches */} + {showRecentSearches && ( +
+
Senaste sökningar
+ {recentSearches.map((person, index) => ( +
itemRefs.current[index] = el} + className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`} + onClick={() => handleSelectPerson(person)} + role="option" + aria-selected={isPersonSelected(person.name)} + > +
+ {person.profilePicture ? ( + {person.name} + ) : ( +
+ {getInitials(person.name)} +
+ )} +
+
+
+ {person.name} + {isPersonSelected(person.name) && } +
+
{person.username}
+
+
+ ))} +
+ )} + + {/* Search Results - Only when typing */} + {showAllPeople && ( + displayPeople.length > 0 ? ( +
+
Sökresultat
+ {displayPeople.map((person, index) => ( +
itemRefs.current[index] = el} + className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`} + onClick={() => handleSelectPerson(person)} + role="option" + aria-selected={isPersonSelected(person.name)} + > +
+ {person.profilePicture ? ( + {person.name} + ) : ( +
+ {getInitials(person.name)} +
+ )} +
+
+
+ {person.name} + {isPersonSelected(person.name) && } +
+
{person.username}
+
+
+ ))} +
+ ) : ( +
+ Inga deltagare hittades +
+ ) + )} +
+ )} +
+ + {/* Selected Participants */} +
+ {/* Default User (Non-deletable) */} +
+ {getCurrentUser().name} +
+ + {/* Additional Participants (Deletable) */} + {booking.participants.map((participant, index) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css new file mode 100644 index 0000000..80c0487 --- /dev/null +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -0,0 +1,310 @@ +.container { + position: relative; +} + +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} + +.selectedParticipants { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0; + margin-top: 1rem; +} + +.participantChip { + display: flex; + align-items: center; + background-color: #F0F8FF; + border: 1px solid #D1E7FF; + border-radius: 1.25rem; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + color: #2563EB; + gap: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + + +.participantName { + font-weight: 500; +} + +.clickableChip { + cursor: pointer; + transition: all 0.2s ease; + width: fit-content; + min-width: auto; +} + +.clickableChip:hover { + background-color: #E0F2FE; + border-color: #BAE6FD; +} + +.clickableChip:focus { + outline: 2px solid #2563EB; + outline-offset: 2px; +} + +.clickableChip:active { + background-color: #BFDBFE; + transform: scale(0.98); +} + + +.removeIcon { + color: #2563EB; + font-size: 0.875rem; + font-weight: bold; + margin-left: 0.25rem; +} + +.defaultUserChip { + background-color: #F3F4F6; + border-color: #D1D5DB; + color: #374151; +} + +.defaultUserChip:hover { + background-color: #F3F4F6; + border-color: #D1D5DB; + color: #374151; + cursor: default; +} + + +.searchContainer { + position: relative; + width: 100%; + max-width: 600px; +} + +.searchInput { + width: 100%; + margin-bottom: 10px; + border: 1px solid #D2D9E0; + border-radius: 0.5rem; + font-size: 16px; + background-color: #FAFBFC; + padding: 1rem; + font-family: inherit; + box-sizing: border-box; +} + +.searchInput::placeholder { + color: #adadad; +} + +.searchInput:focus { + outline: 2px solid #2563EB; + outline-offset: -1px; + border-color: #2563EB; +} + +.dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #D2D9E0; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); + z-index: 1000; + max-height: 300px; + overflow-y: auto; + margin-top: 0rem; +} + +.section { + padding: 0.5rem 0; +} + +.section:not(:last-child) { + border-bottom: 1px solid #F1F3F4; +} + +.sectionHeader { + font-weight: 600; + color: #5F6368; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0.25rem 1rem; + margin-bottom: 0.25rem; +} + +.dropdownItem { + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.2s; + border: none; + border-bottom: 1px solid #F1F3F4; + background: none; + width: 100%; + text-align: left; + font-family: inherit; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.dropdownItem:last-child { + border-bottom: none; +} + +.dropdownItem:hover { + background-color: #F8F9FA; +} + +.dropdownItem:active { + background-color: #E8F0FE; +} + +.personAvatar { + width: 2rem; + height: 2rem; + border-radius: 50%; + flex-shrink: 0; +} + +.avatarImage { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.avatarInitials { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #2563EB; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; +} + +.personInfo { + display: flex; + flex-direction: column; + gap: 0.125rem; + flex: 1; +} + +.personName { + font-weight: 500; + color: #202124; + font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +.personUsername { + font-size: 0.75rem; + color: #5F6368; +} + +.addNewItem { + color: #1A73E8; + font-weight: 500; + opacity: 0.5; + cursor: not-allowed; +} + +.addNewItem:hover { + background-color: transparent; +} + +.selectedItem { + background-color: #F0F8FF; + opacity: 0.7; +} + +.selectedItem:hover { + background-color: #E0F2FE; +} + +.selectedIndicator { + color: #2563EB; + font-weight: bold; + margin-left: 0.5rem; +} + +.focusedItem { + background-color: #2563EB !important; + color: white !important; +} + +.focusedItem .personName { + color: white !important; +} + +.focusedItem .personUsername { + color: rgba(255, 255, 255, 0.8) !important; +} + +.focusedItem .selectedIndicator { + color: white !important; +} + +.focusedItem .avatarInitials { + background-color: rgba(255, 255, 255, 0.2); + color: white; +} + +.noResults { + padding: 1rem; + text-align: center; + color: #5F6368; + font-size: 0.875rem; + font-style: italic; +} + +/* Compact styles */ +.compactContainer { + margin-bottom: 0; +} + +.compactElementHeading { + font-size: 0.75rem; + color: #717171; + font-weight: 500; + margin-bottom: 0.4rem; + margin-top: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.compactSearchInput { + width: 100%; + padding: 0.5rem 1rem; + border: 1px solid #ccc; + border-radius: 0.375rem; + font-size: 16px; + background-color: white; + font-family: inherit; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.compactSearchInput:focus { + outline: 2px solid #007AFF; + outline-offset: 2px; + border-color: #007AFF; +} \ No newline at end of file diff --git a/my-app/src/components/RoomSelectionField.jsx b/my-app/src/components/RoomSelectionField.jsx new file mode 100644 index 0000000..7174dc4 --- /dev/null +++ b/my-app/src/components/RoomSelectionField.jsx @@ -0,0 +1,34 @@ +import React, { useMemo } from 'react'; +import Dropdown from './Dropdown'; +import { SMALL_GROUP_ROOMS } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './RoomSelectionField.module.css'; + +export function RoomSelectionField() { + const booking = useBookingContext(); + const { settings } = useSettingsContext(); + + // Generate room options based on settings + const roomOptions = useMemo(() => { + return Array.from({ length: settings.numberOfRooms }, (_, i) => ({ + value: `G5:${i + 1}`, + label: `G5:${i + 1}`, + })); + }, [settings.numberOfRooms]); + + return ( +
+

Rum

+ booking.handleRoomChange(e)} + placeholder={{ + label: "Alla rum", + value: "allRooms" + }} + /> +
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/RoomSelectionField.module.css b/my-app/src/components/RoomSelectionField.module.css new file mode 100644 index 0000000..c5ad2cc --- /dev/null +++ b/my-app/src/components/RoomSelectionField.module.css @@ -0,0 +1,9 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; +} \ No newline at end of file diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index 92bbe1e..0111367 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -1,11 +1,6 @@ -import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components'; - -import { useState } from 'react'; - +import { Button } from 'react-aria-components'; +import React from 'react'; import styles from './TimeCard.module.css'; - -import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; -import Dropdown from './Dropdown'; import { useBookingContext } from '../context/BookingContext'; export default function TimeCard({ @@ -21,9 +16,9 @@ export default function TimeCard({ const booking = useBookingContext(); let hoursText; - const halfHours = hoursAvailable; - - const [endTimeIndex, setEndTimeIndex] = useState(startTimeIndex + hoursAvailable); + // Use the pre-selected booking length if available, otherwise use available hours + const displayHours = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : hoursAvailable; + const halfHours = displayHours; if (halfHours === 1) { hoursText = "30\u202Fmin"; @@ -35,108 +30,43 @@ export default function TimeCard({ function formatSlotIndex(index) { - const hour = Math.floor(index / 2) + 8; + let hour = Math.floor(index / 2) + 8; + if (hour < 10) { + hour = `0${hour}`; + } const minute = index % 2 === 0 ? "00" : "30"; return `${hour}:${minute}`; } - let classNames = selected ? `${styles.container}` : styles.container; + let classNames = selected ? `${styles.container} ${styles.selected}` : styles.container; const className = state === "unavailableSlot" ? styles.unavailableSlot : styles.availableSlot; const isEndState = ["availableEndTime", "selectedEnd"].includes(state); - 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" }, - ]; - - function getLabelFromAvailableHours(availableHours) { - return bookingLengths.find(option => option.value === availableHours)?.label || "Välj längd"; - } - - 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), - }; - - function handleChange(event) { - console.log(event.target.value); - setEndTimeIndex(startTimeIndex + parseInt(event.target.value)); - booking.setSelectedEndIndex(startTimeIndex + parseInt(event.target.value)); - } if (state === "availableSlot") { return ( - - - - -
- {booking.title == "" ? "Jacobs bokning" : booking.title} -

{convertDateObjectToString(booking.selectedDate)}

-

{getTimeFromIndex(startTimeIndex)} - {getTimeFromIndex(endTimeIndex)}

- -
- - -
- -
- -

G5:12

-
- -
- -

{booking.participants.join(", ")}

-
-
- - -
-
-
-
-
+ ); } diff --git a/my-app/src/components/TimeCard.module.css b/my-app/src/components/TimeCard.module.css index e7f5b8a..98f4a06 100644 --- a/my-app/src/components/TimeCard.module.css +++ b/my-app/src/components/TimeCard.module.css @@ -13,10 +13,41 @@ background-color: #F8FBFC; width: 135px; height: 20px; + transition: all 0.15s ease; } -.container:hover { - background-color: #E5E5E5; +@media (hover: hover) { + .container:hover { + background-color: #E5E5E5; + } +} + +.container:active, +.container[data-pressed] { + background-color: #D1D5DB; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.1s ease; +} + +.container[data-focus-visible] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} + +.selected { + background-color: #2563EB !important; + color: white !important; + border-color: #1d4ed8 !important; + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); +} + +.selected .upToText { + color: rgba(255, 255, 255, 0.8) !important; +} + +.selected .hoursText { + color: white !important; } .container p { @@ -24,6 +55,7 @@ } .startTime { + width: fit-content; font-weight: 600; font-size: 1.3rem; } @@ -31,11 +63,13 @@ .upToText { font-weight: 300; color: #919191; + font-size: 0.9rem } .hoursText { font-weight: 500; color:#686765; + font-size: 0.9rem; } @@ -66,36 +100,53 @@ .cancelButton { flex: 2; - background-color: #ECECEC; + background-color: white; height: 4rem; - color: #5c5454; + color: #374151; font-weight: 600; - border: 1px solid #D2D9E0; + border: 2px solid #d1d5db; border-radius: 0.5rem; + transition: all 0.2s ease; } -.cancelButton:hover, -.saveButton:hover { - cursor: pointer +@media (hover: hover) { + .cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; + } } .saveButton { flex: 3; - background-color: #4C952D; - color: #f2faef; + background-color: #059669; + color: white; height: 4rem; font-weight: 600; font-size: 1.1rem; - border: 1px solid #3C7624; + border: 2px solid #047857; border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); +} + +@media (hover: hover) { + .saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; + } } .saveButton:active { - background-color: #3C7624; + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); } .cancelButton:active { - background-color: #D2D9E0; + background-color: #e5e7eb; + transform: translateY(1px); } .timeSpan { @@ -123,4 +174,76 @@ background-color: white; width: 85%; max-width: 400px; +} + +/* New time display styles */ +.timeDisplay { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.startTime, .endTime { + display: flex; + flex-direction: column; + align-items: center; +} + +.startTime label, .endTime label { + font-size: 0.75rem; + color: #6c757d; + font-weight: 500; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.timeValue { + font-size: 1.5rem; + font-weight: 600; + color: #212529; +} + +.timeValue.placeholder { + color: #adb5bd; + font-style: italic; + font-size: 1rem; +} + +.timeSeparator { + font-size: 1.5rem; + font-weight: 300; + color: #6c757d; + margin: 0 0.5rem; +} + +/* Disabled button styles */ +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + cursor: not-allowed !important; + opacity: 0.6 !important; +} + +@media (hover: hover) { + .disabledButton:hover { + background-color: #f8f9fa !important; + cursor: not-allowed !important; + transform: none !important; + } +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; } \ No newline at end of file diff --git a/my-app/src/components/TimeCardContainer.jsx b/my-app/src/components/TimeCardContainer.jsx index d1c2628..80844ca 100644 --- a/my-app/src/components/TimeCardContainer.jsx +++ b/my-app/src/components/TimeCardContainer.jsx @@ -1,11 +1,20 @@ +import React from 'react'; import TimeCard from './TimeCard'; +import { InlineBookingForm } from './InlineBookingForm'; +import { BookingModal } from './BookingModal'; import styles from './TimeCardContainer.module.css'; +import modalStyles from './BookingModal.module.css'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; const SLOT_GROUPING_SIZE = 8; export function TimeCardContainer() { const booking = useBookingContext(); + const { settings } = useSettingsContext(); + + // Check if we should use inline form + const useInlineForm = settings.bookingFormType === 'inline'; const slotCount = 24; // 12 hours * 2 slots per hour (8:00 to 20:00) const slotIndices = Array.from({ length: slotCount }, (_, i) => i); @@ -41,25 +50,24 @@ export function TimeCardContainer() { } return ( -
- {slotIndiciesToColumns(slotIndices).map((column, index) => { - - return ( -
- { - - column.map(index => { +
+
+ {slotIndiciesToColumns(slotIndices).map((column, index) => { + + return ( +
+ {column.map(slotIndex => { let maxConsecutive = 0; let roomId = ""; if (booking.currentRoom) { - const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, index); + const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex); if (consecutive > maxConsecutive) { maxConsecutive = consecutive; } } else { booking.timeSlotsByRoom.forEach(room => { - const consecutive = countConsecutiveFromSlot(room.times, index); + const consecutive = countConsecutiveFromSlot(room.times, slotIndex); if (consecutive > maxConsecutive) { maxConsecutive = consecutive; roomId = room.roomId; @@ -73,26 +81,88 @@ export function TimeCardContainer() { /* Set time card state here: */ let timeCardState = "unavailableSlot"; - if (maxConsecutive > 0) { - timeCardState = "availableSlot"; - } + + // 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))); - return ( + 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( booking.handleTimeCardClick(index, maxConsecutive, roomId)} - selected={index === booking.selectedStartIndex} + handleClick={() => { + 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 + // Only show inline form if useInlineForm is true + // Cards are laid out in pairs: (0,1), (2,3), (4,5), etc. + if (useInlineForm && 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( + booking.resetTimeSelections()} + arrowPointsLeft={isLeftCard} + /> + ); + } + } + + return elements; + }).flat()}
) })} +
+ + {/* Show modal when a time slot is selected and not using inline form */} + {!useInlineForm && booking.selectedStartIndex !== null && ( + booking.resetTimeSelections()} + isOpen={true} + /> + )}
); } \ No newline at end of file diff --git a/my-app/src/components/TimeCardContainer.module.css b/my-app/src/components/TimeCardContainer.module.css index 5e44012..aa8fd9f 100644 --- a/my-app/src/components/TimeCardContainer.module.css +++ b/my-app/src/components/TimeCardContainer.module.css @@ -13,6 +13,8 @@ width: 350px; gap: 0.5rem; height: fit-content; + align-items: flex-start; + justify-content: center; } .timeCardList { diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index af4eba1..0627e0c 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -19,14 +19,273 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({ })); export const PEOPLE = [ - { id: 1, name: 'Arjohn Emilsson' }, - { id: 2, name: 'Filip Norgren' }, - { id: 3, name: 'Hedvig Engelmark' }, - { id: 4, name: 'Elin Rudling' }, - { id: 5, name: 'Victor Magnusson' }, - { id: 6, name: 'Ellen Britschgi' } + { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@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' }, + { 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: 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: 13, name: 'Linda Svensson', username: 'lisv2174', email: 'linda.svensson@dsv.su.se' }, + { id: 14, name: 'Jonas Gustafsson', username: 'jogu8523', email: 'jonas.gustafsson@dsv.su.se' }, + { id: 15, name: 'Maria Olsson', username: 'maol7456', email: 'maria.olsson@dsv.su.se' }, + { id: 16, name: 'Andreas Berg', username: 'anbe9832', email: 'andreas.berg@dsv.su.se' }, + { id: 17, name: 'Lina Dahlberg', username: 'lida2165', email: 'lina.dahlberg@dsv.su.se' }, + { id: 18, name: 'Marcus Forsberg', username: 'mafo6754', email: 'marcus.forsberg@dsv.su.se' }, + { id: 19, name: 'Julia Hedström', username: 'juhe4921', email: 'julia.hedström@dsv.su.se' }, + { id: 20, name: 'Daniel Lindqvist', username: 'dali8374', email: 'daniel.lindqvist@dsv.su.se' }, + { id: 21, name: 'Sara Blomqvist', username: 'sabl1598', email: 'sara.blomqvist@dsv.su.se' }, + { id: 22, name: 'Henrik Lundberg', username: 'helu7263', email: 'henrik.lundberg@dsv.su.se' }, + { id: 23, name: 'Frida Engström', username: 'fren3847', email: 'frida.engström@dsv.su.se' }, + { id: 24, name: 'Tobias Sjöberg', username: 'tosj5691', email: 'tobias.sjöberg@dsv.su.se' }, + { id: 25, name: 'Amanda Wallin', username: 'amwa9254', email: 'amanda.wallin@dsv.su.se' }, + { id: 26, name: 'Mattias Holm', username: 'maho8173', email: 'mattias.holm@dsv.su.se' }, + { id: 27, name: 'Emelie Sandberg', username: 'emsa4629', email: 'emelie.sandberg@dsv.su.se' }, + { id: 28, name: 'Robin Åberg', username: 'roab7485', email: 'robin.åberg@dsv.su.se' }, + { id: 29, name: 'Caroline Ekström', username: 'caek2916', email: 'caroline.ekström@dsv.su.se' }, + { id: 30, name: 'Alexander Nyström', username: 'alny6387', email: 'alexander.nyström@dsv.su.se' }, + { id: 31, name: 'Lisa Berggren', username: 'libe5142', email: 'lisa.berggren@dsv.su.se' }, + { id: 32, name: 'David Holmberg', username: 'daho8764', email: 'david.holmberg@dsv.su.se' }, + { id: 33, name: 'Maja Lindström', username: 'mali3571', email: 'maja.lindström@dsv.su.se' }, + { id: 34, name: 'Johan Carlsson', username: 'joca6928', email: 'johan.carlsson@dsv.su.se' }, + { id: 35, name: 'Rebecka Svensson', username: 'resv4816', email: 'rebecka.svensson@dsv.su.se' }, + { id: 36, name: 'Niklas Hedberg', username: 'nihe7395', email: 'niklas.hedberg@dsv.su.se' }, + { id: 37, name: 'Ida Persson', username: 'idpe2583', email: 'ida.persson@dsv.su.se' }, + { id: 38, name: 'Martin Öberg', username: 'maöb5947', email: 'martin.öberg@dsv.su.se' }, + { id: 39, name: 'Therese Löfgren', username: 'thlo8162', email: 'therese.löfgren@dsv.su.se' }, + { id: 40, name: 'Stefan Martinsson', username: 'stma4739', email: 'stefan.martinsson@dsv.su.se' }, + { id: 41, name: 'Johanna Stenberg', username: 'jost6254', email: 'johanna.stenberg@dsv.su.se' }, + { id: 42, name: 'Peter Wikström', username: 'pewi9481', email: 'peter.wikström@dsv.su.se' }, + { id: 43, name: 'Mikaela Fransson', username: 'mifr3825', email: 'mikaela.fransson@dsv.su.se' }, + { id: 44, name: 'Christian Lindgren', username: 'chli7694', email: 'christian.lindgren@dsv.su.se' }, + { id: 45, name: 'Evelina Norberg', username: 'evno2147', email: 'evelina.norberg@dsv.su.se' }, + { id: 46, name: 'Andreas Strömberg', username: 'anst5863', email: 'andreas.strömberg@dsv.su.se' }, + { id: 47, name: 'Klara Danielsson', username: 'klda8429', email: 'klara.danielsson@dsv.su.se' }, + { id: 48, name: 'Simon Eklund', username: 'siek6175', email: 'simon.eklund@dsv.su.se' }, + { id: 49, name: 'Elsa Lundgren', username: 'ellu4892', email: 'elsa.lundgren@dsv.su.se' }, + { id: 50, name: 'Mikael Höglund', username: 'mihö7316', email: 'mikael.höglund@dsv.su.se' }, + { id: 51, name: 'Jennie Söderberg', username: 'jesö3754', email: 'jennie.söderberg@dsv.su.se' }, + { id: 52, name: 'Fredrik Åström', username: 'frås9128', email: 'fredrik.åström@dsv.su.se' }, + { id: 53, name: 'Cornelia Sundberg', username: 'cosu5683', email: 'cornelia.sundberg@dsv.su.se' }, + { id: 54, name: 'Emil Palmberg', username: 'empa8297', email: 'emil.palmberg@dsv.su.se' }, + { id: 55, name: 'Josefin Ringström', username: 'jori4612', email: 'josefin.ringström@dsv.su.se' }, + { id: 56, name: 'Christofer Åkesson', username: 'chåk7548', email: 'christofer.åkesson@dsv.su.se' }, + { id: 57, name: 'Agnes Håkansson', username: 'aghå2971', email: 'agnes.håkansson@dsv.su.se' }, + { id: 58, name: 'Sebastian Blomberg', username: 'sebl6394', email: 'sebastian.blomberg@dsv.su.se' }, + { id: 59, name: 'Felicia Gunnarsson', username: 'fegu8756', email: 'felicia.gunnarsson@dsv.su.se' }, + { id: 60, name: 'Jesper Lindahl', username: 'jeli3182', email: 'jesper.lindahl@dsv.su.se' }, + { id: 61, name: 'Sandra Backström', username: 'saba7425', email: 'sandra.backström@dsv.su.se' }, + { id: 62, name: 'William Viklund', username: 'wivi5697', email: 'william.viklund@dsv.su.se' }, + { id: 63, name: 'Lova Arvidsson', username: 'loar9231', email: 'lova.arvidsson@dsv.su.se' }, + { id: 64, name: 'Gabriel Öhman', username: 'gaöh4568', email: 'gabriel.öhman@dsv.su.se' }, + { id: 65, name: 'Isabelle Eriksson', username: 'iser8143', email: 'isabelle.eriksson@dsv.su.se' }, + { id: 66, name: 'Anton Ström', username: 'anst2976', email: 'anton.ström@dsv.su.se' }, + { id: 67, name: 'Wilma Ljungberg', username: 'wilj6314', email: 'wilma.ljungberg@dsv.su.se' }, + { id: 68, name: 'Lucas Sundström', username: 'lusu9587', email: 'lucas.sundström@dsv.su.se' }, + { id: 69, name: 'Hanna Åslund', username: 'haås4729', email: 'hanna.åslund@dsv.su.se' }, + { id: 70, name: 'Pontus Rydberg', username: 'pory8152', email: 'pontus.rydberg@dsv.su.se' }, + { id: 71, name: 'Olivia Nyman', username: 'olny3675', email: 'olivia.nyman@dsv.su.se' }, + { id: 72, name: 'Viktor Östberg', username: 'viös7493', email: 'viktor.östberg@dsv.su.se' }, + { id: 73, name: 'Tilda Forslund', username: 'tifo5128', email: 'tilda.forslund@dsv.su.se' }, + { id: 74, name: 'Carl Holmström', username: 'caho8916', email: 'carl.holmström@dsv.su.se' }, + { id: 75, name: 'Matilda Bengtsson', username: 'mabe4382', email: 'matilda.bengtsson@dsv.su.se' }, + { id: 76, name: 'Alvin Berglund', username: 'albe7654', email: 'alvin.berglund@dsv.su.se' }, + { id: 77, name: 'Saga Nordström', username: 'sano2496', email: 'saga.nordström@dsv.su.se' }, + { id: 78, name: 'Linus Hedström', username: 'lihe6847', email: 'linus.hedström@dsv.su.se' }, + { id: 79, name: 'Elina Jakobsson', username: 'elja9173', email: 'elina.jakobsson@dsv.su.se' }, + { id: 80, name: 'Casper Nordin', username: 'cano3521', email: 'casper.nordin@dsv.su.se' }, + { id: 81, name: 'Nova Malmberg', username: 'noma8765', email: 'nova.malmberg@dsv.su.se' }, + { id: 82, name: 'Isac Björk', username: 'isbj5194', email: 'isac.björk@dsv.su.se' }, + { id: 83, name: 'Ebba Sandström', username: 'ebsa7428', email: 'ebba.sandström@dsv.su.se' }, + { id: 84, name: 'Melvin Åberg', username: 'meåb2683', email: 'melvin.åberg@dsv.su.se' }, + { id: 85, name: 'Astrid Nordahl', username: 'asno6159', email: 'astrid.nordahl@dsv.su.se' }, + { id: 86, name: 'Noel Sjögren', username: 'nosj8437', email: 'noel.sjögren@dsv.su.se' }, + { id: 87, name: 'Linnéa Borg', username: 'libo4826', email: 'linnéa.borg@dsv.su.se' }, + { id: 88, name: 'Adrian Rosén', username: 'adro7195', email: 'adrian.rosén@dsv.su.se' }, + { id: 89, name: 'Smilla Lindberg', username: 'smli3564', email: 'smilla.lindberg@dsv.su.se' }, + { id: 90, name: 'Leon Hammar', username: 'leha9821', email: 'leon.hammar@dsv.su.se' }, + { id: 91, name: 'Ellen Sjöström', username: 'elsj6247', email: 'ellen.sjöström@dsv.su.se' }, + { id: 92, name: 'Tim Hedlund', username: 'tihe4573', email: 'tim.hedlund@dsv.su.se' }, + { id: 93, name: 'Vera Blomgren', username: 'vebl8912', email: 'vera.blomgren@dsv.su.se' }, + { id: 94, name: 'Theodor Larsson', username: 'thla2738', email: 'theodor.larsson@dsv.su.se' }, + { id: 95, name: 'Stella Lundström', username: 'stlu6495', email: 'stella.lundström@dsv.su.se' }, + { id: 96, name: 'Benjamin Engberg', username: 'been8164', email: 'benjamin.engberg@dsv.su.se' }, + { id: 97, name: 'Alicia Rydberg', username: 'alry4827', email: 'alicia.rydberg@dsv.su.se' }, + { id: 98, name: 'Hugo Nordgren', username: 'huno7392', email: 'hugo.nordgren@dsv.su.se' }, + { id: 99, name: 'Moa Stenberg', username: 'most5618', email: 'moa.stenberg@dsv.su.se' }, + { id: 100, name: 'Neo Lindahl', username: 'neli9456', email: 'neo.lindahl@dsv.su.se' }, + { id: 101, name: 'Tess Holm', username: 'teho3274', email: 'tess.holm@dsv.su.se' }, + { id: 102, name: 'Vincent Lundberg', username: 'vilu8593', email: 'vincent.lundberg@dsv.su.se' }, + { id: 103, name: 'Tove Nyberg', username: 'tony6127', email: 'tove.nyberg@dsv.su.se' }, + { id: 104, name: 'Edvin Kvist', username: 'edkv4851', email: 'edvin.kvist@dsv.su.se' }, + { id: 105, name: 'Sigrid Fransson', username: 'sifr7436', email: 'sigrid.fransson@dsv.su.se' }, + { id: 106, name: 'Lovis Hedberg', username: 'lohe2984', email: 'lovis.hedberg@dsv.su.se' }, + { id: 107, name: 'Arvid Stenberg', username: 'arst5627', email: 'arvid.stenberg@dsv.su.se' }, + { id: 108, name: 'June Holmgren', username: 'juho8315', email: 'june.holmgren@dsv.su.se' }, + { id: 109, name: 'Milo Svensson', username: 'misv4762', email: 'milo.svensson@dsv.su.se' }, + { id: 110, name: 'Alice Rosén', username: 'alro6948', email: 'alice.rosén@dsv.su.se' }, + { id: 111, name: 'Viggo Lindström', username: 'vili3591', email: 'viggo.lindström@dsv.su.se' }, + { id: 112, name: 'Thea Östlund', username: 'thös8274', email: 'thea.östlund@dsv.su.se' }, + { id: 113, name: 'Nils Hedström', username: 'nihe5416', email: 'nils.hedström@dsv.su.se' }, + { id: 114, name: 'Signe Dahlberg', username: 'sida7829', email: 'signe.dahlberg@dsv.su.se' }, + { id: 115, name: 'Axel Nordström', username: 'axno2653', email: 'axel.nordström@dsv.su.se' }, + { id: 116, name: 'Amira Al-Hassan', username: 'amal4821', email: 'amira.al-hassan@dsv.su.se' }, + { id: 117, name: 'José García Martínez', username: 'joga7395', email: 'jose.garcia-martinez@dsv.su.se' }, + { id: 118, name: 'Fatima Özkan', username: 'faöz2648', email: 'fatima.özkan@dsv.su.se' }, + { id: 119, name: 'Ahmed Ben Khalil', username: 'ahbe5973', email: 'ahmed.ben-khalil@dsv.su.se' }, + { id: 120, name: 'Elena Popović', username: 'elpo8416', email: 'elena.popovic@dsv.su.se' }, + { id: 121, name: 'Hassan Al-Rashid', username: 'haal3672', email: 'hassan.al-rashid@dsv.su.se' }, + { id: 122, name: 'Mariam Yılmaz', username: 'mayl9254', email: 'mariam.yilmaz@dsv.su.se' }, + { id: 123, name: 'Nicolas Müller Schmidt', username: 'nimu4817', email: 'nicolas.muller-schmidt@dsv.su.se' }, + { id: 124, name: 'Layla Abdallah', username: 'laab6539', email: 'layla.abdallah@dsv.su.se' }, + { id: 125, name: 'Dragan Milosević', username: 'drmi8142', email: 'dragan.milosevic@dsv.su.se' }, + { id: 126, name: 'Aïsha Benali', username: 'aibe2785', email: 'aisha.benali@dsv.su.se' }, + { id: 127, name: 'Omar El-Khoury', username: 'omkh5396', email: 'omar.el-khoury@dsv.su.se' }, + { id: 128, name: 'Željana Petković', username: 'žepe7614', email: 'zeljana.petkovic@dsv.su.se' }, + { id: 129, name: 'Mohammed Al-Zahra', username: 'moal3928', email: 'mohammed.al-zahra@dsv.su.se' }, + { id: 130, name: 'Francesca Di Marco', username: 'frdi6157', email: 'francesca.di-marco@dsv.su.se' }, + { id: 131, name: 'Kemal Özdogan', username: 'keöz8473', email: 'kemal.ozdogan@dsv.su.se' }, + { id: 132, name: 'Rania Mansour', username: 'rama4692', email: 'rania.mansour@dsv.su.se' }, + { id: 133, name: 'Miloš Jovanović', username: 'mijo7285', email: 'milos.jovanovic@dsv.su.se' }, + { id: 134, name: 'Yasmina Benchaib', username: 'yabe5814', email: 'yasmina.benchaib@dsv.su.se' }, + { id: 135, name: 'Andrés López Rivera', username: 'anlo3647', email: 'andres.lopez-rivera@dsv.su.se' }, + { id: 136, name: 'Nour Al-Mahmoud', username: 'noal9172', email: 'nour.al-mahmoud@dsv.su.se' }, + { id: 137, name: 'Goran Nikolić', username: 'goni6428', email: 'goran.nikolic@dsv.su.se' }, + { id: 138, name: 'Ines García López', username: 'inga4753', email: 'ines.garcia-lopez@dsv.su.se' }, + { id: 139, name: 'Yusuf Çelik', username: 'yuçe8596', email: 'yusuf.celik@dsv.su.se' }, + { id: 140, name: 'Maryam Hosseini', username: 'maho2319', email: 'maryam.hosseini@dsv.su.se' }, + { id: 141, name: 'Stjepan Kovačević', username: 'stko7684', email: 'stjepan.kovacevic@dsv.su.se' }, + { id: 142, name: 'Samira Bouzid', username: 'sabo5127', email: 'samira.bouzid@dsv.su.se' }, + { id: 143, name: 'Dimitri Papadopoulos', username: 'dipa8941', email: 'dimitri.papadopoulos@dsv.su.se' }, + { id: 144, name: 'Leïla Benabbas', username: 'lebe3465', email: 'leila.benabbas@dsv.su.se' }, + { id: 145, name: 'Aleksandar Stanković', username: 'alst6782', email: 'aleksandar.stankovic@dsv.su.se' }, + { id: 146, name: 'Carmen Ruiz Vega', username: 'caru9218', email: 'carmen.ruiz-vega@dsv.su.se' }, + { id: 147, name: 'Tariq Al-Masri', username: 'taal4576', email: 'tariq.al-masri@dsv.su.se' }, + { id: 148, name: 'Nataša Đorđević', username: 'naðo7839', email: 'natasa.djordjevic@dsv.su.se' }, + { id: 149, name: 'Ibrahim Kaya', username: 'ibka2153', email: 'ibrahim.kaya@dsv.su.se' }, + { id: 150, name: 'Soraya Ahmadi', username: 'soah5694', email: 'soraya.ahmadi@dsv.su.se' }, + { id: 151, name: 'Nikola Bogdanović', username: 'nibo8327', email: 'nikola.bogdanovic@dsv.su.se' }, + { id: 152, name: 'Lucía Hernández Silva', username: 'luhe6471', email: 'lucia.hernandez-silva@dsv.su.se' }, + { id: 153, name: 'Mehmet Güler', username: 'megu3795', email: 'mehmet.guler@dsv.su.se' }, + { id: 154, name: 'Zahra Mortazavi', username: 'zamo9148', email: 'zahra.mortazavi@dsv.su.se' }, + { id: 155, name: 'Petar Živković', username: 'peži4672', email: 'petar.zivkovic@dsv.su.se' }, + { id: 156, name: 'Inés Moreno Castro', username: 'inmo7286', email: 'ines.moreno-castro@dsv.su.se' }, + { id: 157, name: 'Salam Al-Faraj', username: 'saal5539', email: 'salam.al-faraj@dsv.su.se' }, + { id: 158, name: 'Tijana Milić', username: 'timi8713', email: 'tijana.milic@dsv.su.se' }, + { id: 159, name: 'Raúl Jiménez Torres', username: 'raji2467', email: 'raul.jimenez-torres@dsv.su.se' }, + { id: 160, name: 'Bana Al-Qasemi', username: 'baal6824', email: 'bana.al-qasemi@dsv.su.se' }, + { id: 161, name: 'Marko Simić', username: 'masi4195', email: 'marko.simic@dsv.su.se' }, + { id: 162, name: 'Álvaro González Díaz', username: 'algo7658', email: 'alvaro.gonzalez-diaz@dsv.su.se' }, + { id: 163, name: 'Hala Kassem', username: 'haka3912', email: 'hala.kassem@dsv.su.se' }, + { id: 164, name: 'Damir Petrović', username: 'dape9275', email: 'damir.petrovic@dsv.su.se' }, + { id: 165, name: 'Esperanza Rivera Morales', username: 'esri5438', email: 'esperanza.rivera-morales@dsv.su.se' }, + { id: 166, name: 'Rashid Al-Nouri', username: 'raal8164', email: 'rashid.al-nouri@dsv.su.se' }, + { id: 167, name: 'Milica Vuković', username: 'mivu6729', email: 'milica.vukovic@dsv.su.se' }, + { id: 168, name: 'Cristóbal Herrera López', username: 'crhe4583', email: 'cristobal.herrera-lopez@dsv.su.se' }, + { id: 169, name: 'Amina Benali', username: 'ambe7296', email: 'amina.benali@dsv.su.se' }, + { id: 170, name: 'Saša Radović', username: 'sara9851', email: 'sasa.radovic@dsv.su.se' }, + { id: 171, name: 'Adrián Vargas Ruiz', username: 'adva3467', email: 'adrian.vargas-ruiz@dsv.su.se' }, + { id: 172, name: 'Laith Al-Khouri', username: 'laal6142', email: 'laith.al-khouri@dsv.su.se' }, + { id: 173, name: 'Jovana Stojanović', username: 'jost8375', email: 'jovana.stojanovic@dsv.su.se' }, + { id: 174, name: 'Sebastián Méndez Torres', username: 'seme5698', email: 'sebastian.mendez-torres@dsv.su.se' }, + { id: 175, name: 'Dina Al-Rashid', username: 'dial2914', email: 'dina.al-rashid@dsv.su.se' }, + { id: 176, name: 'Viktor Đokić', username: 'viðo7427', email: 'viktor.djokic@dsv.su.se' }, + { id: 177, name: 'Paloma Santos García', username: 'pasa4851', email: 'paloma.santos-garcia@dsv.su.se' }, + { id: 178, name: 'Khalil Mahmoud', username: 'khma6293', email: 'khalil.mahmoud@dsv.su.se' }, + { id: 179, name: 'Anja Matić', username: 'anma9576', email: 'anja.matic@dsv.su.se' }, + { id: 180, name: 'Eduardo Ramírez Silva', username: 'edra3184', email: 'eduardo.ramirez-silva@dsv.su.se' }, + { id: 181, name: 'Yasmin Al-Qadri', username: 'yaal7629', email: 'yasmin.al-qadri@dsv.su.se' }, + { id: 182, name: 'Nemanja Vasić', username: 'neva5472', email: 'nemanja.vasic@dsv.su.se' }, + { id: 183, name: 'Sofía Castillo Moreno', username: 'soca8196', email: 'sofia.castillo-moreno@dsv.su.se' }, + { id: 184, name: 'Fadi Haddad', username: 'faha2758', email: 'fadi.haddad@dsv.su.se' }, + { id: 185, name: 'Dragana Marković', username: 'drma6341', email: 'dragana.markovic@dsv.su.se' }, + { id: 186, name: 'Matías Herrera Vega', username: 'mahe9487', email: 'matias.herrera-vega@dsv.su.se' }, + { id: 187, name: 'Lina Al-Zahra', username: 'lial4725', email: 'lina.al-zahra@dsv.su.se' }, + { id: 188, name: 'Stefan Đurić', username: 'stðu7853', email: 'stefan.djuric@dsv.su.se' }, + { id: 189, name: 'Valentina López García', username: 'valo3619', email: 'valentina.lopez-garcia@dsv.su.se' }, + { id: 190, name: 'Samir Nasser', username: 'sana8274', email: 'samir.nasser@dsv.su.se' }, + { id: 191, name: 'Ana Milanović', username: 'anmi6527', email: 'ana.milanovic@dsv.su.se' }, + { id: 192, name: 'Gabriel Fernández Ruiz', username: 'gafe4892', email: 'gabriel.fernandez-ruiz@dsv.su.se' }, + { id: 193, name: 'Rana Al-Masri', username: 'raal7156', email: 'rana.al-masri@dsv.su.se' }, + { id: 194, name: 'Dejan Nikolić', username: 'deni3481', email: 'dejan.nikolic@dsv.su.se' }, + { id: 195, name: 'Isabella Torres Díaz', username: 'isto8675', email: 'isabella.torres-diaz@dsv.su.se' }, + { id: 196, name: 'Omar Benali', username: 'ombe5239', email: 'omar.benali@dsv.su.se' }, + { id: 197, name: 'Milena Stanković', username: 'mist9164', email: 'milena.stankovic@dsv.su.se' }, + { id: 198, name: 'Alejandro Morales Castro', username: 'almo2748', email: 'alejandro.morales-castro@dsv.su.se' }, + { id: 199, name: 'Nadia Farouk', username: 'nafa6583', email: 'nadia.farouk@dsv.su.se' }, + { id: 200, name: 'Bojan Petrović', username: 'bope7926', email: 'bojan.petrovic@dsv.su.se' }, + { id: 201, name: 'Camila Sánchez Torres', username: 'casa4371', email: 'camila.sanchez-torres@dsv.su.se' }, + { id: 202, name: 'Yousef Al-Ahmad', username: 'yoal8649', email: 'yousef.al-ahmad@dsv.su.se' }, + { id: 203, name: 'Teodora Jovanović', username: 'tejo5194', email: 'teodora.jovanovic@dsv.su.se' }, + { id: 204, name: 'Diego Herrera Martín', username: 'dihe9738', email: 'diego.herrera-martin@dsv.su.se' }, + { id: 205, name: 'Farah Al-Qasimi', username: 'faal3526', email: 'farah.al-qasimi@dsv.su.se' }, + { id: 206, name: 'Luka Mitrović', username: 'lumi7284', email: 'luka.mitrovic@dsv.su.se' }, + { id: 207, name: 'Natalia Guzmán Silva', username: 'nagu6417', email: 'natalia.guzman-silva@dsv.su.se' }, + { id: 208, name: 'Karim Al-Rashid', username: 'kaal8952', email: 'karim.al-rashid@dsv.su.se' }, + { id: 209, name: 'Maja Stojanović', username: 'mast4635', email: 'maja.stojanovic@dsv.su.se' }, + { id: 210, name: 'Emilio García Pérez', username: 'emga7158', email: 'emilio.garcia-perez@dsv.su.se' }, + { id: 211, name: 'Layla Mansouri', username: 'lama2794', email: 'layla.mansouri@dsv.su.se' }, + { id: 212, name: 'Marija Đorđević', username: 'maðo6381', email: 'marija.djordjevic@dsv.su.se' }, + { id: 213, name: 'Ricardo Vásquez López', username: 'riva9517', email: 'ricardo.vasquez-lopez@dsv.su.se' }, + { id: 214, name: 'Zeinab Al-Khoury', username: 'zeal5743', email: 'zeinab.al-khoury@dsv.su.se' }, + { id: 215, name: 'Filip Kostić', username: 'fiko8296', email: 'filip.kostic@dsv.su.se' }, + + // Really long but realistic names + { id: 216, name: 'María Esperanza del Carmen Rodríguez-Fernández y Pérez-González', username: 'maes4829', email: 'maria.esperanza.rodriguez-fernandez@dsv.su.se' }, + { id: 217, name: 'Alexandros Konstantinos Dimitrios Papadopoulos-Georgiou', username: 'alko7395', email: 'alexandros.konstantinos.papadopoulos@dsv.su.se' }, + { id: 218, name: 'Jean-Baptiste François-Xavier de la Croix-Montmorency', username: 'jefr2648', email: 'jean-baptiste.de-la-croix@dsv.su.se' }, + { id: 219, name: 'Ana-María Concepción Guadalupe Hernández-Vásquez de los Santos', username: 'anco5973', email: 'ana-maria.hernandez-vasquez@dsv.su.se' }, + { id: 220, name: 'Christophoros Theodoros Athanasios Michalopoulos-Stavropoulos', username: 'chth8416', email: 'christophoros.michalopoulos@dsv.su.se' }, + { id: 221, name: 'José María Antonio Francisco Javier García-Sánchez y Martínez-López', username: 'joma3672', email: 'jose.maria.garcia-sanchez@dsv.su.se' }, + { id: 222, name: 'Elisabeth Charlotte Francoise Marie-Antoinette von Habsburg-Lorraine', username: 'elch9254', email: 'elisabeth.von-habsburg@dsv.su.se' }, + { id: 223, name: 'Pedro Alfonso Ramón Fernando Díez-Canseco y Herrera-Mendoza', username: 'peal4817', email: 'pedro.alfonso.diez-canseco@dsv.su.se' }, + { id: 224, name: 'Anastasia Ekaterini Theodora Constantinidou-Papadimitriou', username: 'anek6539', email: 'anastasia.constantinidou@dsv.su.se' }, + { id: 225, name: 'Francesco Giuseppe Alessandro Maria Benedetto di Savoia-Carignano', username: 'frgi8142', email: 'francesco.di-savoia@dsv.su.se' }, + + // English names + { id: 226, name: 'Oliver Pemberton-Wells', username: 'olpe2785', email: 'oliver.pemberton-wells@dsv.su.se' }, + { id: 227, name: 'Charlotte Ashworth', username: 'chas5396', email: 'charlotte.ashworth@dsv.su.se' }, + { id: 228, name: 'Henry Fitzpatrick', username: 'hefi7614', email: 'henry.fitzpatrick@dsv.su.se' }, + { id: 229, name: 'Imogen Blackwood', username: 'imbl3928', email: 'imogen.blackwood@dsv.su.se' }, + { id: 230, name: 'Rupert Cholmondeley', username: 'ruch6157', email: 'rupert.cholmondeley@dsv.su.se' }, + { id: 231, name: 'Arabella Thornbury', username: 'arth8473', email: 'arabella.thornbury@dsv.su.se' }, + { id: 232, name: 'Benedict Worthington', username: 'bewo4692', email: 'benedict.worthington@dsv.su.se' }, + { id: 233, name: 'Cordelia Featherstone', username: 'cofe7285', email: 'cordelia.featherstone@dsv.su.se' }, + { id: 234, name: 'Jasper Windermere', username: 'jawi5814', email: 'jasper.windermere@dsv.su.se' }, + { id: 235, name: 'Penelope Harrington-Lloyd', username: 'peha3647', email: 'penelope.harrington-lloyd@dsv.su.se' }, + + // Norwegian names + { id: 236, name: 'Magnus Bjørklund', username: 'mabj9172', email: 'magnus.bjorklund@dsv.su.se' }, + { id: 237, name: 'Astrid Haugen', username: 'asha6428', email: 'astrid.haugen@dsv.su.se' }, + { id: 238, name: 'Lars Øvrebø', username: 'laøv4753', email: 'lars.ovrebo@dsv.su.se' }, + { id: 239, name: 'Ingrid Løvdal', username: 'inlø8596', email: 'ingrid.lovdal@dsv.su.se' }, + { id: 240, name: 'Erik Åsheim', username: 'erås2319', email: 'erik.asheim@dsv.su.se' }, + { id: 241, name: 'Kari Fjellheim', username: 'kafj7684', email: 'kari.fjellheim@dsv.su.se' }, + { id: 242, name: 'Olav Størdahl', username: 'olst5127', email: 'olav.stordahl@dsv.su.se' }, + { id: 243, name: 'Silje Rønning', username: 'sir8941', email: 'silje.ronning@dsv.su.se' }, + { id: 244, name: 'Torstein Bråten', username: 'tobr3465', email: 'torstein.braten@dsv.su.se' }, + { id: 245, name: 'Gunnhild Skjælaaen', username: 'gusk6782', email: 'gunnhild.skjaelaaen@dsv.su.se' }, + + // American names + { id: 246, name: 'Hunter McKenzie III', username: 'humc9218', email: 'hunter.mckenzie@dsv.su.se' }, + { id: 247, name: 'Madison Taylor-Brooks', username: 'mata4576', email: 'madison.taylor-brooks@dsv.su.se' }, + { id: 248, name: 'Braxton Washington Jr.', username: 'brwa7839', email: 'braxton.washington@dsv.su.se' }, + { id: 249, name: 'Skylar Kennedy-Davis', username: 'skke2153', email: 'skylar.kennedy-davis@dsv.su.se' }, + { id: 250, name: 'Preston Montgomery IV', username: 'prmo5694', email: 'preston.montgomery@dsv.su.se' } ]; +export const USER = { + id: 1, + name: 'Jacob Reinikainen', + username: 'jare2473', + email: 'jacob.reinikainen@dsv.su.se' +}; + export const DEFAULT_DISABLED_OPTIONS = { 1: false, 2: false, diff --git a/my-app/src/context/BookingContext.jsx b/my-app/src/context/BookingContext.jsx index b162629..33966c8 100644 --- a/my-app/src/context/BookingContext.jsx +++ b/my-app/src/context/BookingContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import React, { createContext, useContext } from 'react'; const BookingContext = createContext(null); diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx new file mode 100644 index 0000000..b287737 --- /dev/null +++ b/my-app/src/context/SettingsContext.jsx @@ -0,0 +1,146 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { today, getLocalTimeZone, CalendarDate } from '@internationalized/date'; +import { USER } from '../constants/bookingConstants'; + +const SettingsContext = createContext(); + +export const useSettingsContext = () => { + const context = useContext(SettingsContext); + if (!context) { + throw new Error('useSettingsContext must be used within a SettingsProvider'); + } + return context; +}; + +export const SettingsProvider = ({ children }) => { + const [settings, setSettings] = useState(() => { + // Load settings from localStorage or use defaults + const saved = localStorage.getItem('calendarSettings'); + if (saved) { + try { + const parsed = JSON.parse(saved); + return { + // Set defaults first + mockToday: null, + bookingRangeDays: 14, + roomAvailabilityChance: 0.7, + numberOfRooms: 5, + earliestTimeSlot: 0, + latestTimeSlot: 23, + currentUserName: USER.name, + showDevelopmentBanner: false, + showBookingConfirmationBanner: false, + showBookingDeleteBanner: false, + bookingFormType: 'inline', // 'modal' or 'inline' + // Then override with saved values + ...parsed, + // Convert date strings back to DateValue objects + mockToday: parsed.mockToday ? new Date(parsed.mockToday) : null, + // Ensure currentUserName has a fallback + currentUserName: parsed.currentUserName || USER.name, + }; + } catch (e) { + console.warn('Failed to parse saved settings:', e); + } + } + + return { + // Use mock date if set, otherwise real today + mockToday: null, + // Days in the future users can book + bookingRangeDays: 14, + // Room availability percentage + roomAvailabilityChance: 0.7, + // Number of rooms + numberOfRooms: 5, + // Earliest booking time (in half-hour slots from 8:00) + earliestTimeSlot: 0, // 8:00 + // Latest booking time + latestTimeSlot: 23, // 19:30 (last slot ending at 20:00) + // Current user settings + currentUserName: USER.name, + // Development banner toggle + showDevelopmentBanner: false, + // Booking confirmation banner toggle + showBookingConfirmationBanner: false, + // Booking delete banner toggle + showBookingDeleteBanner: false, + // Booking form type + bookingFormType: 'inline', // 'modal' or 'inline' + }; + }); + + // Save settings to localStorage whenever they change + useEffect(() => { + const toSave = { + ...settings, + // Convert Date objects to strings for JSON serialization + mockToday: settings.mockToday ? settings.mockToday.toISOString() : null, + }; + localStorage.setItem('calendarSettings', JSON.stringify(toSave)); + }, [settings]); + + // Get the effective "today" date (mock or real) + const getEffectiveToday = () => { + if (settings.mockToday) { + // Convert JavaScript Date to CalendarDate using the proper library function + const mockDate = settings.mockToday; + const year = mockDate.getFullYear(); + const month = mockDate.getMonth() + 1; // JS months are 0-indexed + const day = mockDate.getDate(); + + return new CalendarDate(year, month, day); + } + return today(getLocalTimeZone()); + }; + + const updateSettings = (newSettings) => { + setSettings(prev => ({ ...prev, ...newSettings })); + }; + + const resetSettings = () => { + setSettings({ + mockToday: null, + bookingRangeDays: 14, + roomAvailabilityChance: 0.7, + numberOfRooms: 5, + earliestTimeSlot: 0, + latestTimeSlot: 23, + currentUserName: USER.name, // This will reset to "Jacob Reinikainen" + showDevelopmentBanner: false, + showBookingConfirmationBanner: false, + showBookingDeleteBanner: false, + bookingFormType: 'inline', + }); + localStorage.removeItem('calendarSettings'); + }; + + // Get current user as participant object + const getCurrentUser = () => { + return { + id: USER.id, + name: settings.currentUserName, + username: USER.username, + email: USER.email + }; + }; + + // Get dynamic default booking title + const getDefaultBookingTitle = () => { + const firstName = settings.currentUserName.split(' ')[0]; + return `${firstName}s bokning`; + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/my-app/src/helpers.jsx b/my-app/src/helpers.jsx index 57f6d84..35aa97d 100644 --- a/my-app/src/helpers.jsx +++ b/my-app/src/helpers.jsx @@ -4,28 +4,38 @@ export function getTimeFromIndex(timeIndex) { const totalHalfHoursFromStart = timeIndex; const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base - const hours = Math.floor(totalMinutes / 60); + let hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; + if (hours < 10) { + hours = `0${hours}`; + } + return `${hours}:${minutes === 0 ? '00' : '30'}`; } export function convertDateObjectToString( date ) { - - const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; const dayIndex = getDayOfWeek(date, "en-US"); - - const dayOfWeek = (dayIndex >= 1 && dayIndex <= 7) ? days[dayIndex - 1] : "Ogiltig dag"; - - const months = [ - "Januari", "Februari", "Mars", "April", "Maj", "Juni", - "Juli", "Augusti", "September", "Oktober", "November", "December" - ]; - const monthIndex = date.month; - const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; - - - return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`; - return ""; + + // Always use long format for now + const isSmallScreen = false; + + if (isSmallScreen) { + const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"]; + const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"]; + + const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag"; + const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; + + return `${dayOfWeek} ${date.day} ${monthName}`; + } else { + const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; + const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"]; + + const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag"; + const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; + + return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`; + } } \ No newline at end of file diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index d0feb89..e77b0c4 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -6,19 +6,44 @@ import { generateId, findObjectById } from '../utils/bookingUtils'; -import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants'; +import { PEOPLE, USER } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; +import { useSettingsContext } from '../context/SettingsContext'; -export function useBookingState(addBooking) { +function getRoomCategory(roomName) { + // Extract room number from room name (e.g., "G5:7" -> 7) + const roomNumber = parseInt(roomName.split(':')[1]); + + // Assign categories based on room number ranges + 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'; + + // Default fallback + return 'green'; +} + +export function useBookingState(addBooking, initialDate = null) { + const { settings, getDefaultBookingTitle } = useSettingsContext(); + // State hooks - simplified back to useState for stability - const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(generateInitialRooms()); + const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(() => + generateInitialRooms( + settings.roomAvailabilityChance, + settings.numberOfRooms, + settings.earliestTimeSlot, + settings.latestTimeSlot + ) + ); const [currentRoom, setCurrentRoom] = useState(null); const [selectedRoom, setSelectedRoom] = useState("allRooms"); + const [assignedRoom, setAssignedRoom] = useState(null); const [selectedStartIndex, setSelectedStartIndex] = useState(null); const [selectedEndIndex, setSelectedEndIndex] = useState(null); const [selectedBookingLength, setSelectedBookingLength] = useState(0); const [participant, setParticipant] = useState("Arjohn Emilsson"); - const [selectedDate, setSelectedDate] = useState(today(getLocalTimeZone())); + const [selectedDate, setSelectedDate] = useState(initialDate || today(getLocalTimeZone())); const [availableTimeSlots, setAvailableTimeSlots] = useState([]); const [indeciesInHover, setIndeciesInHover] = useState([]); const [title, setTitle] = useState(""); @@ -35,6 +60,7 @@ export function useBookingState(addBooking) { setAvailableTimeSlots([]); setSelectedStartIndex(null); setSelectedEndIndex(null); + setAssignedRoom(null); }, []); const resetSelections = useCallback(() => { @@ -48,16 +74,22 @@ export function useBookingState(addBooking) { }, []); const handleTimeCardClick = useCallback((startHour, hoursAvailable, roomId) => { + console.log('TimeCard clicked:', { startHour, hoursAvailable, roomId }); setSelectedStartIndex(startHour); setSelectedEndIndex(startHour + hoursAvailable); - setSelectedRoom(roomId); + setAssignedRoom(roomId); }, []); const handleDateChange = useCallback((date) => { setSelectedDate(date); - setTimeSlotsByRoom(generateInitialRooms()); + setTimeSlotsByRoom(generateInitialRooms( + settings.roomAvailabilityChance, + settings.numberOfRooms, + settings.earliestTimeSlot, + settings.latestTimeSlot + )); resetTimeSelections(); - }, [resetTimeSelections]); + }, [resetTimeSelections, settings.roomAvailabilityChance, settings.numberOfRooms, settings.earliestTimeSlot, settings.latestTimeSlot]); const handleRoomChange = useCallback((event) => { const roomValue = event.target.value; @@ -77,20 +109,36 @@ export function useBookingState(addBooking) { }, [resetTimeSelections]); const handleSave = useCallback(() => { + // Use assignedRoom for the booking, not selectedRoom + const roomToBook = selectedRoom !== "allRooms" ? selectedRoom : assignedRoom; + + console.log('Saving booking with:', { + selectedStartIndex, + selectedEndIndex, + room: roomToBook, + title + }); + + // Include the current user as a participant if not already added + const allParticipants = participants.find(p => p.id === USER.id) + ? participants + : [USER, ...participants]; + addBooking({ id: generateId(), date: selectedDate, startTime: selectedStartIndex, endTime: selectedEndIndex, - room: selectedRoom, - title: title !== "" ? title : DEFAULT_BOOKING_TITLE, - participants: participants + room: roomToBook, + roomCategory: getRoomCategory(roomToBook), + title: title !== "" ? title : getDefaultBookingTitle(), + participants: allParticipants }); resetSelections(); navigate('/'); window.scrollTo(0, 0); - }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, title, participants, resetSelections, navigate]); + }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, assignedRoom, title, participants, resetSelections, navigate, getDefaultBookingTitle]); const handleTimeCardExit = useCallback(() => { if (!selectedEndIndex) { @@ -98,11 +146,24 @@ export function useBookingState(addBooking) { } }, [selectedEndIndex]); - const handleParticipantChange = useCallback((participant) => { - if (participant !== null) { - setParticipants(prev => [...prev, participant.trim()]); + const handleParticipantChange = useCallback((participantId) => { + console.log('handleParticipantChange called with:', participantId); + if (participantId !== null && participantId !== undefined) { + // Find the person by ID and add the full person object + const person = PEOPLE.find(p => p.id === participantId); + console.log('Found person:', person); + if (person && !participants.find(p => p.id === person.id)) { + console.log('Adding participant:', person.name); + setParticipants(prev => [...prev, person]); + } else { + console.log('Participant already exists or person not found'); + } setParticipant(""); } + }, [participants]); + + const handleRemoveParticipant = useCallback((participantToRemove) => { + setParticipants(prev => prev.filter(p => p.id !== participantToRemove.id)); }, []); // Memoize the return object to prevent unnecessary re-renders @@ -111,6 +172,7 @@ export function useBookingState(addBooking) { timeSlotsByRoom, currentRoom, selectedRoom, + assignedRoom, selectedStartIndex, selectedEndIndex, selectedBookingLength, @@ -134,10 +196,13 @@ export function useBookingState(addBooking) { handleSave, handleTimeCardExit, handleParticipantChange, + handleRemoveParticipant, + resetTimeSelections, }), [ timeSlotsByRoom, currentRoom, selectedRoom, + assignedRoom, selectedStartIndex, selectedEndIndex, selectedBookingLength, @@ -155,5 +220,7 @@ export function useBookingState(addBooking) { handleSave, handleTimeCardExit, handleParticipantChange, + handleRemoveParticipant, + resetTimeSelections, ]); } \ No newline at end of file diff --git a/my-app/src/icons/CalendarIcon.jsx b/my-app/src/icons/CalendarIcon.jsx new file mode 100644 index 0000000..d12ba45 --- /dev/null +++ b/my-app/src/icons/CalendarIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function CalendarIcon({ color = '#666', size = 16 }) { + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/my-app/src/icons/ChevronLeft.jsx b/my-app/src/icons/ChevronLeft.jsx new file mode 100644 index 0000000..f3f5eef --- /dev/null +++ b/my-app/src/icons/ChevronLeft.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function ChevronLeft({ className, color, disabled, ...props }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/my-app/src/icons/ChevronRight.jsx b/my-app/src/icons/ChevronRight.jsx new file mode 100644 index 0000000..dabe0ad --- /dev/null +++ b/my-app/src/icons/ChevronRight.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function ChevronRight({ className, color, disabled, ...props }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/my-app/src/main.jsx b/my-app/src/main.jsx index b9a1a6d..0d0f6e9 100644 --- a/my-app/src/main.jsx +++ b/my-app/src/main.jsx @@ -1,4 +1,4 @@ -import { StrictMode } from 'react' +import React, { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx new file mode 100644 index 0000000..2b2e67b --- /dev/null +++ b/my-app/src/pages/BookingSettings.jsx @@ -0,0 +1,321 @@ +import React, { useState } from 'react'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './BookingSettings.module.css'; + +export function BookingSettings() { + const { settings, updateSettings, resetSettings, getEffectiveToday } = useSettingsContext(); + const [tempDate, setTempDate] = useState(''); + + const handleMockDateChange = (e) => { + const dateValue = e.target.value; + setTempDate(dateValue); + if (dateValue) { + updateSettings({ mockToday: new Date(dateValue) }); + } else { + updateSettings({ mockToday: null }); + } + }; + + const formatDateForInput = (date) => { + if (!date) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + const getTimeFromSlot = (slot) => { + const totalMinutes = 8 * 60 + slot * 30; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return `${hours}:${minutes === 0 ? '00' : '30'}`; + }; + + const effectiveToday = getEffectiveToday(); + const isUsingMockDate = settings.mockToday !== null; + + return ( +
+
+

Booking Settings

+

Configure booking system behavior for testing purposes

+
+ +
+
+

User Settings

+ +
+ + updateSettings({ currentUserName: e.target.value })} + className={styles.textInput} + placeholder="Enter your name" + /> +
+
+ +
+

Display Settings

+ +
+ +
+ updateSettings({ showDevelopmentBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showDevelopmentBanner ? 'Enabled' : 'Disabled'} + +
+
+ +
+ +
+ updateSettings({ showBookingConfirmationBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showBookingConfirmationBanner ? 'Enabled' : 'Disabled'} + +
+
+ +
+ +
+ updateSettings({ showBookingDeleteBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showBookingDeleteBanner ? 'Enabled' : 'Disabled'} + +
+
+ +
+ + +
+ Current: {settings.bookingFormType === 'inline' ? 'Inline Form' : 'Modal Popup'} +
+
+
+ +
+

Date Settings

+ +
+ +
+ + {isUsingMockDate && ( + + )} +
+
+ Effective today: {effectiveToday.day}/{effectiveToday.month}/{effectiveToday.year} + {isUsingMockDate && (MOCK)} +
+
+ +
+ + updateSettings({ bookingRangeDays: parseInt(e.target.value) })} + className={styles.numberInput} + /> +
+ Latest bookable date: {effectiveToday.add({ days: settings.bookingRangeDays }).day}/{effectiveToday.add({ days: settings.bookingRangeDays }).month}/{effectiveToday.add({ days: settings.bookingRangeDays }).year} +
+
+
+ +
+

Room Settings

+ +
+ + updateSettings({ numberOfRooms: parseInt(e.target.value) })} + className={styles.numberInput} + /> +
+ +
+ +
+ updateSettings({ roomAvailabilityChance: parseFloat(e.target.value) })} + className={styles.slider} + /> + + {Math.round(settings.roomAvailabilityChance * 100)}% + +
+
+
+ +
+

Time Slot Settings

+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ Settings are automatically saved and will persist between sessions +
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css new file mode 100644 index 0000000..9e70107 --- /dev/null +++ b/my-app/src/pages/BookingSettings.module.css @@ -0,0 +1,306 @@ +.container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + font-family: system-ui, -apple-system, sans-serif; +} + +.header { + margin-bottom: 2rem; + text-align: center; +} + +.header h1 { + font-size: 2.5rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.5rem; +} + +.header p { + color: #6b7280; + font-size: 1.1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.section { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.section h2 { + font-size: 1.5rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 1.5rem; + border-bottom: 2px solid #f3f4f6; + padding-bottom: 0.5rem; +} + +.setting { + margin-bottom: 1.5rem; +} + +.setting:last-child { + margin-bottom: 0; +} + +.setting label { + display: block; + margin-bottom: 0.5rem; +} + +.setting label strong { + font-size: 1rem; + font-weight: 600; + color: #374151; + display: block; +} + +.description { + font-size: 0.875rem; + color: #6b7280; + margin-top: 0.25rem; + display: block; +} + +.dateGroup { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.dateInput, .numberInput, .select, .textInput { + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background: white; +} + +.dateInput:focus, .numberInput:focus, .select:focus, .textInput:focus { + outline: none; + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.dateInput { + width: 200px; +} + +.numberInput { + width: 120px; +} + +.select { + width: 150px; +} + +.textInput { + width: 250px; +} + +.clearButton { + padding: 0.5rem 1rem; + background: #f3f4f6; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + color: #374151; + cursor: pointer; + transition: all 0.2s; + width: fit-content; +} + +.clearButton:hover { + background: #e5e7eb; + border-color: #9ca3af; +} + +.currentStatus { + margin-top: 0.5rem; + font-size: 0.875rem; + color: #4b5563; +} + +.mockLabel { + background: #fbbf24; + color: #92400e; + padding: 0.125rem 0.375rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.sliderGroup { + display: flex; + align-items: center; + gap: 1rem; +} + +.slider { + flex: 1; + height: 6px; + border-radius: 3px; + background: #e5e7eb; + outline: none; + cursor: pointer; + -webkit-appearance: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #2563eb; + cursor: pointer; + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.slider::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #2563eb; + cursor: pointer; + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.sliderValue { + font-weight: 600; + color: #2563eb; + min-width: 40px; + text-align: center; +} + +.toggleGroup { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.toggle { + width: 50px; + height: 24px; + background: #d1d5db; + border-radius: 12px; + border: none; + position: relative; + cursor: pointer; + transition: background-color 0.3s ease; + -webkit-appearance: none; + appearance: none; +} + +.toggle:checked { + background: #10b981; +} + +.toggle:before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background: white; + top: 2px; + left: 2px; + transition: transform 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.toggle:checked:before { + transform: translateX(26px); +} + +.toggleStatus { + font-weight: 500; + color: #374151; +} + +.actions { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center;; + gap: 1rem; + padding: 2rem; + background: #f9fafb; + border-radius: 12px; + border: 1px solid #e5e7eb; + width: 100%; +} + +.resetButton { + padding: 0.75rem 2rem; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + width: fit-content; +} + +.resetButton:hover { + background: #b91c1c; +} + +.testSessionButton { + padding: 0.75rem 2rem; + background: #2563eb; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + width: fit-content; +} + +.testSessionButton:hover { + background: #1d4ed8; +} + +.info { + font-size: 0.875rem; + color: #6b7280; + text-align: center; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .section { + padding: 1rem; + } + + .dateGroup { + flex-direction: column; + align-items: flex-start; + } + + .sliderGroup { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 4f928a5..af1ba53 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -1,28 +1,119 @@ +import React, { useState, useEffect } from 'react'; import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/TimeCardContainer'; import { BookingDatePicker } from '../components/BookingDatePicker'; -import { BookingFormFields } from '../components/BookingFormFields'; +import { BookingTitleField } from '../components/BookingTitleField'; +import { ParticipantsSelector } from '../components/ParticipantsSelector'; +import { RoomSelectionField } from '../components/RoomSelectionField'; +import { BookingLengthField } from '../components/BookingLengthField'; import { useBookingState } from '../hooks/useBookingState'; import { BookingProvider } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; export function NewBooking({ addBooking }) { - const booking = useBookingState(addBooking); + const { getEffectiveToday, settings } = useSettingsContext(); + const booking = useBookingState(addBooking, getEffectiveToday()); + const [showFilters, setShowFilters] = useState(false); + + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + // Check if we should use inline form (hide title and participants from main form) + const useInlineForm = settings.bookingFormType === 'inline'; + + // Check if any filters are active + const hasActiveFilters = booking.selectedRoom !== "allRooms" || booking.selectedBookingLength > 0; + + // Generate filter display text + const getFilterText = () => { + const filters = []; + + if (booking.selectedRoom !== "allRooms") { + filters.push(booking.selectedRoom); + } + + if (booking.selectedBookingLength > 0) { + const bookingLengths = { + 1: "30 min", + 2: "1 h", + 3: "1.5 h", + 4: "2 h", + 5: "2.5 h", + 6: "3 h", + 7: "3.5 h", + 8: "4 h" + }; + filters.push(bookingLengths[booking.selectedBookingLength]); + } + + if (filters.length === 0) return "Filter"; + return `Filter (${filters.join(", ")})`; + }; + + // Reset all filters + const handleResetFilters = () => { + booking.handleRoomChange({ target: { value: "allRooms" } }); + booking.handleLengthChange(0); + setShowFilters(false); + }; return (
+

Boka litet grupprum

-
+ {/* Only show title and participants fields in modal mode */} + {!useInlineForm && ( + <> + + + + )} + +
- -
- -

- Lediga tider -

-
- + + {/* Filter Button */} +
+ + + {/* Collapsible Filter Content */} + {showFilters && ( +
+
+ + +
+ {hasActiveFilters && ( +
+ +
+ )} +
+ )} +
+ +

+ Lediga tider +

+
+ +
diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index d76fc17..9a3b2c4 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -1,5 +1,6 @@ .pageContainer { padding: 1rem; + background-color: white; } .formContainer { @@ -15,6 +16,16 @@ font-weight: 529; } +.bookingTimesContainer { + margin-top: 2rem; + padding: 2rem; + border-radius: 0.3rem; + outline: 1px solid #E7E7E7; + display: flex; + flex-direction: column; + align-items: center; +} + .modalFooter { width: 100%; @@ -131,4 +142,129 @@ background-color: rgb(216, 216, 216); width: 100%; margin: 1rem 0; +} + +/* Filter Section Styles */ +.filtersSection { + width: 100%; + margin-top: 1rem; + margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.filterButton { + width: fit-content; + background: white; + border: 1px solid #D1D5DB; + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.filterButton:hover { + background: #F9FAFB; + border-color: #9CA3AF; +} + +.filterButton:active { + background: #F3F4F6; +} + +.activeFilter { + background: #EBF8FF !important; + border-color: #3B82F6 !important; + color: #1E40AF !important; +} + +.activeFilter:hover { + background: #DBEAFE !important; + border-color: #2563EB !important; +} + +.filterIcon { + font-size: 1rem; +} + +.chevron { + font-size: 0.75rem; + color: #6B7280; + transition: transform 0.2s ease; +} + +.chevronUp { + transform: rotate(0deg); +} + +.chevronDown { + transform: rotate(0deg); +} + +.filtersContent { + width: fit-content; + max-width: 600px; + padding: 1rem; + background: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 0.5rem; + animation: slideDown 0.2s ease-out; +} + +.filtersRow { + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; +} + +.resetSection { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #E5E7EB; + display: flex; + justify-content: center; +} + +.resetButton { + background: white; + border: 1px solid #DC2626; + color: #DC2626; + border-radius: 0.375rem; + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.resetButton:hover { + background: #FEF2F2; + border-color: #B91C1C; + color: #B91C1C; +} + +.resetButton:active { + background: #FEE2E2; + transform: translateY(1px); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 22ff4a1..13f0c48 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -1,21 +1,54 @@ +import React, { useEffect } from 'react'; import styles from './RoomBooking.module.css'; import { Link } from 'react-router-dom'; import BookingsList from '../components/BookingsList'; import Card from '../components/Card'; +import { useSettingsContext } from '../context/SettingsContext'; +import { USER } from '../constants/bookingConstants'; -export function RoomBooking({ bookings }) { +export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) { + const { settings } = useSettingsContext(); + + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const isTestSessionActive = settings.currentUserName !== USER.name; function handleEditBooking(booking) { console.log(booking); - setIsEditBooking(booking); + // setIsEditBooking(booking); // This line seems to have an error, commenting out } return (
+ {isTestSessionActive && ( +
+
+

Välkommen, {settings.currentUserName}!

+

Hantera dina bokningar och reservera nya lokaler

+
+
+ )}

Lokalbokning

-

Mina bokingar

- -

Ny bokning

+

Mina bokingar

+ +
+

Ny bokning

diff --git a/my-app/src/pages/RoomBooking.module.css b/my-app/src/pages/RoomBooking.module.css index 5ed8c45..2aaeff8 100644 --- a/my-app/src/pages/RoomBooking.module.css +++ b/my-app/src/pages/RoomBooking.module.css @@ -7,4 +7,92 @@ font-size: 2.6rem; font-weight: 300; font-family: 'The Sans', system-ui, sans-serif; +} + +.sectionHeading { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.sectionDivider { + border: none; + border-top: 1px solid #dedede; + margin-bottom: 2rem; +} + +.welcomeSection { + display: flex; + align-items: center; + justify-content: space-between; + background: #3d50a8; + padding: 2rem 2.5rem; + margin: 1.5rem 0 2.5rem 0; + color: white; + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.15); + position: relative; + overflow: hidden; +} + +.welcomeSection::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 100px; + height: 100px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + border-radius: 50%; + transform: translate(20px, -20px); +} + +.welcomeContent { + flex: 1; +} + +.welcomeTitle { + font-size: 1.8rem; + font-weight: 700; + margin: 0; + font-family: 'The Sans', system-ui, sans-serif; + letter-spacing: -0.5px; +} + +.welcomeSubtitle { + font-size: 1rem; + margin: 0.5rem 0 0 0; + opacity: 0.9; + font-weight: 400; +} + +.welcomeIcon { + width: 48px; + height: 48px; + opacity: 0.8; + color: white; +} + +.welcomeIcon svg { + width: 100%; + height: 100%; +} + +@media (max-width: 768px) { + .welcomeSection { + padding: 1.5rem 1.8rem; + margin: 1rem 0 2rem 0; + } + + .welcomeTitle { + font-size: 1.5rem; + } + + .welcomeSubtitle { + font-size: 0.9rem; + } + + .welcomeIcon { + width: 40px; + height: 40px; + } } \ No newline at end of file diff --git a/my-app/src/pages/TestSession.jsx b/my-app/src/pages/TestSession.jsx new file mode 100644 index 0000000..e3a1d6c --- /dev/null +++ b/my-app/src/pages/TestSession.jsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './TestSession.module.css'; + +export function TestSession() { + const [userName, setUserName] = useState(''); + const { updateSettings } = useSettingsContext(); + const navigate = useNavigate(); + + const handleCustomNameChange = (e) => { + setUserName(e.target.value); + setIsUsingNormalName(false); + }; + + const handleStart = () => { + if (userName.trim()) { + // Update the user name in settings + updateSettings({ currentUserName: userName.trim() }); + // Navigate to the main app + navigate('/'); + } + }; + + const canStart = userName.trim().length > 0; + + return ( +
+
+
+

Testsession

+

Välkommen! Ange ditt namn för att börja:

+
+ +
+ +
+ {canStart && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/pages/TestSession.module.css b/my-app/src/pages/TestSession.module.css new file mode 100644 index 0000000..acd9561 --- /dev/null +++ b/my-app/src/pages/TestSession.module.css @@ -0,0 +1,110 @@ +.container { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.content { + max-width: 500px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.header h1 { + font-size: 2rem; + color: #1f2937; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.header p { + font-size: 1rem; + color: #6b7280; + margin-bottom: 2rem; +} + +.nameSection { + margin: 2rem 0; + width: 100%; +} + +.nameInput { + width: 100%; + padding: 0.75rem 1rem; + font-size: 16px; + border: 1px solid #d1d5db; + border-radius: 6px; + outline: none; + margin-bottom: 1.5rem; + -webkit-text-size-adjust: 100%; + -webkit-appearance: none; +} + +.nameInput:focus { + border-color: #2563eb; +} + +.normalNames { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; +} + +.normalNameButton { + padding: 0.5rem 1rem; + background: white; + border: 1px solid #d1d5db; + border-radius: 4px; + font-size: 0.9rem; + color: #374151; + cursor: pointer; + margin: 0.25rem; +} + +.normalNameButton:hover { + background: #f9fafb; +} + +.normalNameButton.selected { + background: #2563eb; + border-color: #2563eb; + color: white; +} + +.startButton { + padding: 0.75rem 2rem; + background: #2563eb; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; + margin-top: 1.5rem; + width: fit-content; +} + +.startButton:hover { + background: #1d4ed8; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .normalNames { + flex-direction: column; + align-items: center; + } + + .normalNameButton { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file diff --git a/my-app/src/react-aria-starter/src/Calendar.css b/my-app/src/react-aria-starter/src/Calendar.css index 0b91bf7..3a9e38d 100644 --- a/my-app/src/react-aria-starter/src/Calendar.css +++ b/my-app/src/react-aria-starter/src/Calendar.css @@ -23,6 +23,10 @@ width: 2rem; height: 2rem; padding: 0; + + &[data-disabled] { + display: none; + } } .react-aria-CalendarCell { @@ -39,6 +43,10 @@ display: none; } + &:hover:not([data-selected]):not([data-disabled]):not([data-unavailable]) { + background-color: #e9e9e9; + } + &[data-pressed] { background: var(--gray-100); } @@ -64,6 +72,7 @@ &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); + color: rgb(203, 203, 203); } } diff --git a/my-app/src/react-aria-starter/src/ComboBox.css b/my-app/src/react-aria-starter/src/ComboBox.css index 33051f0..c91af2f 100644 --- a/my-app/src/react-aria-starter/src/ComboBox.css +++ b/my-app/src/react-aria-starter/src/ComboBox.css @@ -27,6 +27,8 @@ outline: none; min-width: 0; font-family: inherit; + width: 100%; + max-width: 600px; } .combo-box-input[data-focused] { diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index ef00219..5e17c75 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -13,16 +13,52 @@ display: flex; width: fit-content; align-items: center; + width: fit-content; + gap: 2rem; } .react-aria-Button { - background: var(--highlight-background); - color: var(--highlight-foreground); + min-width: fit-content; + } + + .chevron-button { + background: none; + border: none; + padding: 0.5rem; + margin: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s, opacity 0.2s; + } + + .chevron-button:hover:not(:disabled) { + background-color: rgba(0, 0, 0, 0.05); + } + + .chevron-button:active:not(:disabled) { + background-color: rgba(0, 0, 0, 0.1); + } + + .chevron-button:disabled { + cursor: default; + } + + .chevron-button:focus-visible { + outline: 2px solid #2563EB; + outline-offset: 2px; + } + + .react-aria-Button { + /*background: var(--highlight-background);*/ + /*color: var(--highlight-foreground);*/ border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; - border: none; - margin-left: -1.929rem; + /*border: none;*/ + border: 1px solid #D4D4D4; /*width: 1.429rem;*/ /*height: 1.429rem;*/ width: fit-content; @@ -32,7 +68,8 @@ &[data-pressed] { box-shadow: none; - background: var(--highlight-background); + /*background: var(--highlight-background);*/ + background: #f1f1f1; } &[data-focus-visible] { @@ -41,6 +78,55 @@ } } + .calendar-button { + min-width: 220px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + gap: 0.75rem !important; + cursor: pointer !important; + background: white !important; + border: 1px solid #E5E7EB !important; + border-radius: 8px !important; + padding: 12px 16px !important; + font-weight: 500 !important; + color: #374151 !important; + transition: all 0.2s ease !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + white-space: nowrap !important; + } + + @media (max-width: 640px) { + .calendar-button { + min-width: 200px !important; + } + } + + .calendar-button:hover { + border-color: #D1D5DB !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; + } + + .calendar-button[data-pressed] { + background: #F9FAFB !important; + border-color: #9CA3AF !important; + transform: translateY(1px) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + } + + .calendar-button[data-focus-visible] { + outline: 2px solid #2563EB !important; + outline-offset: 2px !important; + border-color: #2563EB !important; + } + + .calendar-date { + flex: 1; + text-align: left; + font-size: 14px; + line-height: 1.25; + } + .react-aria-DateInput { padding: 4px 2.5rem 4px 8px; } @@ -48,6 +134,8 @@ .react-aria-Popover[data-trigger=DatePicker] { max-width: unset; + transform: translateX(-50%); + left: 50% !important; } .react-aria-DatePicker { diff --git a/my-app/src/react-aria-starter/src/DatePicker.tsx b/my-app/src/react-aria-starter/src/DatePicker.tsx index 36e57ab..5fb9f8a 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.tsx +++ b/my-app/src/react-aria-starter/src/DatePicker.tsx @@ -21,24 +21,41 @@ import { import './DatePicker.css'; import { convertDateObjectToString } from '../../helpers'; +import ChevronLeft from '../../icons/ChevronLeft'; +import ChevronRight from '../../icons/ChevronRight'; +import CalendarIcon from '../../icons/CalendarIcon'; export interface DatePickerProps extends AriaDatePickerProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); + chevronColor?: string; + canNavigatePrevious?: boolean; + canNavigateNext?: boolean; + onPreviousClick?: () => void; + onNextClick?: () => void; } export function DatePicker( - { label, description, errorMessage, firstDayOfWeek, ...props }: - DatePickerProps + { + label, + description, + errorMessage, + firstDayOfWeek, + chevronColor = "#666", + canNavigatePrevious = true, + canNavigateNext = true, + onPreviousClick, + onNextClick, + ...props + }: DatePickerProps ) { return ( ( - { /* @@ -47,8 +64,32 @@ export function DatePicker( */ } - - + + + + + {description && {description}} {errorMessage} diff --git a/my-app/src/react-aria-starter/src/Modal.css b/my-app/src/react-aria-starter/src/Modal.css index f8fec11..589190d 100644 --- a/my-app/src/react-aria-starter/src/Modal.css +++ b/my-app/src/react-aria-starter/src/Modal.css @@ -25,7 +25,6 @@ .react-aria-Modal { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); - border-radius: 6px; background: var(--overlay-background); color: var(--text-color); border: 1px solid var(--gray-400); diff --git a/my-app/src/react-aria-starter/src/theme.css b/my-app/src/react-aria-starter/src/theme.css index 247f02c..b4bc1e3 100644 --- a/my-app/src/react-aria-starter/src/theme.css +++ b/my-app/src/react-aria-starter/src/theme.css @@ -10,10 +10,10 @@ * Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ :root { --background-color: #f8f8f8; - --gray-50: #ffffff; + --gray-50: #FAFBFC; --gray-100: #d0d0d0; --gray-200: #afafaf; - --gray-300: #8f8f8f; + --gray-300: #CECECE; --gray-400: #717171; --gray-500: #555555; --gray-600: #393939; @@ -40,7 +40,7 @@ --gray-50: #101010; --gray-100: #393939; --gray-200: #4f4f4f; - --gray-300: #686868; + --gray-300: #CECECE; --gray-400: #848484; --gray-500: #a7a7a7; --gray-600: #cfcfcf; @@ -83,7 +83,7 @@ --button-background-pressed: var(--background-color); /* these colors are the same between light and dark themes * to ensure contrast with the foreground color */ - --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ + --highlight-background: #3e70ec; /* purple-300 from dark theme, 3.03:1 against background-color */ --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ --highlight-foreground: white; /* 5.56:1 against highlight-background */ diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index fb81d9b..d8537a2 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -1,11 +1,12 @@ import { today, getLocalTimeZone } from '@internationalized/date'; import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY } from '../constants/bookingConstants'; -export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS) => { +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}`, - times: Array.from({ length: 23 }, (_, i) => ({ - available: Math.random() < chanceOfAvailability ? true : false + times: Array.from({ length: 24 }, (_, i) => ({ + available: i >= earliestSlot && i <= latestSlot ? + (Math.random() < chanceOfAvailability) : false })) })); }; @@ -74,15 +75,14 @@ export const getLongestConsecutive = (allRooms) => { return longest; }; -export const createDisabledRanges = () => { - const now = today(getLocalTimeZone()); +export const createDisabledRanges = (effectiveToday, bookingRangeDays = 14) => { return [ - [now.add({ days: 14 }), now.add({ days: 9999 })], + [effectiveToday.add({ days: bookingRangeDays }), effectiveToday.add({ days: 9999 })], ]; }; -export const isDateUnavailable = (date) => { - const disabledRanges = createDisabledRanges(); +export const isDateUnavailable = (date, effectiveToday, bookingRangeDays = 14) => { + const disabledRanges = createDisabledRanges(effectiveToday, bookingRangeDays); return disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 diff --git a/my-app/vite.config.js b/my-app/vite.config.js index cf92bbd..2dea53a 100644 --- a/my-app/vite.config.js +++ b/my-app/vite.config.js @@ -1,38 +1,7 @@ -/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' -// https://vite.dev/config/ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; -const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - -// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - basePath: '/~jare2473', - test: { - projects: [{ - extends: true, - plugins: [ - // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - storybookTest({ - configDir: path.join(dirname, '.storybook') - })], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - provider: 'playwright', - instances: [{ - browser: 'chromium' - }] - }, - setupFiles: ['.storybook/vitest.setup.js'] - } - }] - } -}); \ No newline at end of file +}) \ No newline at end of file