diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 76e0a30..0ce08f6 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -6,6 +6,7 @@ import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; import { BookingDetails } from './pages/BookingDetails'; +import { BookingConfirmation } from './pages/BookingConfirmation'; import { BookingSettings } from './pages/BookingSettings'; import { CourseSchedule } from './pages/CourseSchedule'; import { CourseScheduleView } from './pages/CourseScheduleView'; @@ -121,6 +122,7 @@ const AppRoutes = () => { setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} /> } /> } /> + } /> } /> } /> } /> diff --git a/my-app/src/components/booking/BookingsList.jsx b/my-app/src/components/booking/BookingsList.jsx index 91a2b99..369c426 100644 --- a/my-app/src/components/booking/BookingsList.jsx +++ b/my-app/src/components/booking/BookingsList.jsx @@ -5,12 +5,15 @@ import BookingCard from './BookingCard'; import NotificationBanner from '../common/NotificationBanner'; 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; + // Sort bookings by date (earliest first) + const sortedBookings = [...bookings].sort((a, b) => { + // Convert dates to comparable values + const dateA = new Date(a.date.year, a.date.month - 1, a.date.day, 0, a.startTime * 0.5 + 8); + const dateB = new Date(b.date.year, b.date.month - 1, b.date.day, 0, b.startTime * 0.5 + 8); + return dateA - dateB; + }); function handleBookingClick(booking) { setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id); @@ -68,9 +71,9 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD /> )}
- {bookings.length > 0 ? ( + {sortedBookings.length > 0 ? ( <> - {displayedBookings.map((booking, index) => ( + {sortedBookings.map((booking, index) => ( ))} - {hasMoreBookings && ( - - )} ) : (

Du har inga bokningar just nu

diff --git a/my-app/src/components/booking/InlineModalBookingForm.module.css b/my-app/src/components/booking/InlineModalBookingForm.module.css index 17294bc..380b380 100644 --- a/my-app/src/components/booking/InlineModalBookingForm.module.css +++ b/my-app/src/components/booking/InlineModalBookingForm.module.css @@ -10,6 +10,7 @@ flex-basis: 100%; max-width: none; position: relative; + z-index: 1; } /* Arrow pointing to left card */ @@ -63,10 +64,11 @@ } .formHeader { - text-align: center; + text-align: start; /*margin-bottom: var(--spacing-2xl);*/ /*padding-bottom: var(--spacing-lg);*/ /*border-bottom: 1px solid var(--border-light);*/ + margin-bottom: 1rem; } .formTitle { @@ -83,7 +85,7 @@ } .section { - margin-bottom: var(--spacing-2xl); + margin-bottom: 1.4rem; } .formField { @@ -93,9 +95,9 @@ } .formField label { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - color: var(--text-primary); + font-size: 0.75rem; + font-weight: 500; + color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.5px; } @@ -107,19 +109,26 @@ .formActions { display: flex; gap: var(--spacing-lg); + height: fit-content; /*margin-top: var(--spacing-3xl);*/ /*padding-top: var(--spacing-2xl);*/ /*border-top: 1px solid var(--border-light);*/ } +.confirmationActions { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + .cancelButton { flex: 1; background-color: var(--modal-cancel-bg); height: 2.75rem; color: var(--modal-cancel-text); font-weight: var(--font-weight-semibold); - border: 2px solid var(--modal-cancel-border); - border-radius: var(--border-radius-md); + border: 1px solid var(--modal-cancel-border); + border-radius: var(--border-radius-sm); transition: var(--transition-medium); cursor: pointer; font-size: var(--font-size-sm); @@ -138,24 +147,30 @@ .saveButton { flex: 2; background-color: var(--modal-save-bg); + + background-color: #245CF8; + background-color: #1745E8; color: var(--modal-save-text); height: 2.75rem; font-weight: var(--font-weight-semibold); font-size: var(--font-size-sm); border: 2px solid var(--modal-save-border); - border-radius: var(--border-radius-md); + border: 2px solid #0A3CC5; + border-radius: var(--border-radius-sm); transition: var(--transition-medium); - box-shadow: var(--modal-save-shadow); cursor: pointer; } .saveButton:hover { background-color: var(--modal-save-hover-bg); - box-shadow: var(--modal-save-hover-shadow); + background-color: #052FC8; + border: 2px solid #0B2FAF; } .saveButton:active { background-color: var(--modal-save-active-bg); + background-color: #072698; + border: 2px solid #092072; transform: translateY(1px); box-shadow: var(--modal-save-active-shadow); } diff --git a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx new file mode 100644 index 0000000..23c29db --- /dev/null +++ b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button } from 'react-aria-components'; +import { useNavigate } from 'react-router-dom'; +import { convertDateObjectToString, getTimeFromIndex, formatBookingDate } from '../../helpers'; +import Dropdown from '../ui/Dropdown'; +import { Chip } from '../ui/Chip'; +import { BookingTitleField } from '../forms/BookingTitleField'; +import { ParticipantsSelector } from '../forms/ParticipantsSelector'; +import { useBookingContext } from '../../context/BookingContext'; +import { useSettingsContext } from '../../context/SettingsContext'; +import { generateId } from '../../utils/bookingUtils'; +import { USER } from '../../constants/bookingConstants'; +import styles from './InlineModalBookingForm.module.css'; +import extendedStyles from './InlineModalExtendedBookingForm.module.css'; + +// Helper function to get room category +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 InlineModalExtendedBookingForm({ + startTimeIndex, + hoursAvailable, + endTimeIndex, + setEndTimeIndex, + onClose, + onNavigateToDetails, + addBooking, + arrowPointsLeft = true +}) { + const navigate = useNavigate(); + const booking = useBookingContext(); + const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext(); + + // Initialize with pre-selected end time if available, or auto-select if only 30 min available + const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength : + (hoursAvailable === 1 ? startTimeIndex + 1 : null); // Auto-select 30 min if that's all that's available + const [selectedEndTimeIndex, setSelectedEndTimeIndex] = useState(null); + const [showConfirmation, setShowConfirmation] = useState(false); + const hasInitialized = useRef(false); + const confirmationRef = useRef(null); + + // Store the original hours available to prevent it from changing when selections are made + const originalHoursAvailable = useRef(hoursAvailable); + if (originalHoursAvailable.current < hoursAvailable) { + originalHoursAvailable.current = hoursAvailable; + } + + // Effect to handle initial setup only once when form opens + useEffect(() => { + if (initialEndTimeIndex && !hasInitialized.current) { + setSelectedEndTimeIndex(initialEndTimeIndex); + setEndTimeIndex(initialEndTimeIndex); + booking.setSelectedEndIndex(initialEndTimeIndex); + hasInitialized.current = true; + } + + console.log("Booking:", booking); + }, [initialEndTimeIndex, setEndTimeIndex, booking]); + + // Effect to scroll to confirmation when it's shown + useEffect(() => { + if (showConfirmation && confirmationRef.current) { + // Small delay to ensure the DOM has updated + setTimeout(() => { + confirmationRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + }, 100); + } + }, [showConfirmation]); + + // Effect to ensure only the correct component shows confirmation + useEffect(() => { + // Only show confirmation if this component is for the currently selected time slot + if (booking.selectedStartIndex !== startTimeIndex && showConfirmation) { + setShowConfirmation(false); + } + }, [booking.selectedStartIndex, startTimeIndex, showConfirmation]); + + // Effect to reset confirmation on component mount if not the selected time + useEffect(() => { + if (booking.selectedStartIndex !== startTimeIndex) { + setShowConfirmation(false); + } + }, []); // Only run on mount + + // Generate end time options based on available hours + const endTimeOptions = []; + const disabledOptions = {}; + + // Always show all possible options up to the original available hours, not limited by current selection + const maxOptions = Math.min(originalHoursAvailable.current, 8); + + for (let i = 1; i <= maxOptions; i++) { + const endTimeIndex = startTimeIndex + i; + const endTime = getTimeFromIndex(endTimeIndex); + const durationLabel = i === 1 ? "30 min" : + i === 2 ? "1 h" : + i === 3 ? "1.5 h" : + i === 4 ? "2 h" : + i === 5 ? "2.5 h" : + i === 6 ? "3 h" : + i === 7 ? "3.5 h" : + i === 8 ? "4 h" : `${i * 0.5} h`; + + endTimeOptions.push({ + value: endTimeIndex, + label: `${endTime} · ${durationLabel}` + }); + + disabledOptions[endTimeIndex] = false; // All available options are enabled + } + + function handleChange(event) { + const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedEndTimeIndex(endTimeValue); + + if (endTimeValue !== null) { + setEndTimeIndex(endTimeValue); + booking.setSelectedEndIndex(endTimeValue); + // Update the selected booking length in context so it doesn't interfere + const newLength = endTimeValue - startTimeIndex; + booking.setSelectedBookingLength && booking.setSelectedBookingLength(newLength); + } else { + // Reset to default state when placeholder is selected + setEndTimeIndex(startTimeIndex); + booking.setSelectedEndIndex(null); + booking.setSelectedBookingLength && booking.setSelectedBookingLength(0); + } + } + + // Check if user has selected an end time (including pre-selected) + const hasSelectedEndTime = selectedEndTimeIndex !== null; + + const handleSave = () => { + if (hasSelectedEndTime && addBooking) { + console.log('Booking context state:', { + title: booking.title, + participants: booking.participants, + selectedRoom: booking.selectedRoom, + assignedRoom: booking.assignedRoom + }); + + // Create a booking object with the same logic as in useBookingState + const roomToBook = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom; + + // Include the current user as a participant if not already added + const allParticipants = booking.participants.find(p => p.id === USER.id) + ? booking.participants + : [USER, ...booking.participants]; + + const finalTitle = booking.title !== "" ? booking.title : getDefaultBookingTitle(); + + const newBooking = { + id: generateId(), + date: booking.selectedDate, + startTime: booking.selectedStartIndex, + endTime: booking.selectedEndIndex, + room: roomToBook, + roomCategory: getRoomCategory(roomToBook), + title: finalTitle, + participants: allParticipants + }; + + console.log('Creating booking:', newBooking); + console.log('Final title used:', finalTitle); + + // Save the booking using the passed addBooking function + addBooking(newBooking); + + // Show confirmation page within the modal instead of navigating + setShowConfirmation(true); + } + }; + + // Show confirmation page if user pressed save AND this is the active time slot + if (showConfirmation && booking.selectedStartIndex === startTimeIndex) { + return ( +
+
+
+

Bokning sparad!

+

+ {booking.title ? `${booking.title}` : `${getDefaultBookingTitle()}`} +

+ {(() => { + // Include the current user as a participant if not already added + const allParticipants = booking.participants.find(p => p.id === USER.id) + ? booking.participants + : [USER, ...booking.participants]; + + const startTime = getTimeFromIndex(booking.selectedStartIndex); + const endTime = getTimeFromIndex(booking.selectedEndIndex); + const dateStr = formatBookingDate(booking.selectedDate); + const roomName = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom; + + return ( +
+

+ {dateStr} • {startTime} - {endTime} +

+
+ Rum: + + {roomName} + +
+ {allParticipants.length > 0 && ( +

+ Deltagare: {allParticipants.map(p => p.name).join(', ')} +

+ )} +
+ ); + })()} +
+
+ +
+ +
+ + +
+
+ ); + } + + return ( +
+ {/* Header */} +
+ {/* Time Selection */} +
+
+ + +
+
+ + {/* Title Field - Compact */} +
+ +
+ + {/* Participants Field - Compact */} +
+ +
+
+ +
+ + {/* Actions */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/booking/InlineModalExtendedBookingForm.module.css b/my-app/src/components/booking/InlineModalExtendedBookingForm.module.css new file mode 100644 index 0000000..56693aa --- /dev/null +++ b/my-app/src/components/booking/InlineModalExtendedBookingForm.module.css @@ -0,0 +1,46 @@ +/* Import base styles from the regular inline modal form */ +@import './InlineModalBookingForm.module.css'; + +/* Ensure all form inputs have consistent height */ +.compactInput { + height: 2.5rem; /* Match dropdown height */ + display: flex; + align-items: center; + box-sizing: border-box; +} + +/* Override compact styles to match dropdown height exactly */ +:global(.compactTextInput), +:global(.compactSearchInput) { + height: 2.5rem !important; + display: flex !important; + align-items: center !important; + padding: 0.5rem 1rem !important; + box-sizing: border-box !important; +} + +/* Ensure headings are aligned */ +.compactHeading { + margin-bottom: 0.4rem; + margin-top: 0; +} + +.divider { + margin: 1.5rem 0; + border: 0.6px solid var(--border-light); +} + +.confirmationActions { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; +} + +.confirmationActions .saveButton, +.confirmationActions .cancelButton { + height: 2.75rem !important; + min-height: 2.75rem !important; + flex: none !important; + width: 100% !important; +} \ No newline at end of file diff --git a/my-app/src/components/booking/InlineModalExtendedBookingFormNoLabels.jsx b/my-app/src/components/booking/InlineModalExtendedBookingFormNoLabels.jsx new file mode 100644 index 0000000..4b418ef --- /dev/null +++ b/my-app/src/components/booking/InlineModalExtendedBookingFormNoLabels.jsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button } from 'react-aria-components'; +import { useNavigate } from 'react-router-dom'; +import { convertDateObjectToString, getTimeFromIndex } from '../../helpers'; +import Dropdown from '../ui/Dropdown'; +import { BookingTitleField } from '../forms/BookingTitleField'; +import { ParticipantsSelector } from '../forms/ParticipantsSelector'; +import { useBookingContext } from '../../context/BookingContext'; +import { useSettingsContext } from '../../context/SettingsContext'; +import styles from './InlineModalBookingForm.module.css'; +import extendedStyles from './InlineModalExtendedBookingForm.module.css'; + +export function InlineModalExtendedBookingFormNoLabels({ + startTimeIndex, + hoursAvailable, + endTimeIndex, + setEndTimeIndex, + onClose, + onNavigateToDetails, + arrowPointsLeft = true +}) { + const navigate = useNavigate(); + const booking = useBookingContext(); + const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext(); + + // Initialize with pre-selected end time if available, or auto-select if only 30 min available + const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength : + (hoursAvailable === 1 ? startTimeIndex + 1 : null); // Auto-select 30 min if that's all that's available + const [selectedEndTimeIndex, setSelectedEndTimeIndex] = useState(null); + const hasInitialized = useRef(false); + + // Store the original hours available to prevent it from changing when selections are made + const originalHoursAvailable = useRef(hoursAvailable); + if (originalHoursAvailable.current < hoursAvailable) { + originalHoursAvailable.current = hoursAvailable; + } + + // Effect to handle initial setup only once when form opens + useEffect(() => { + if (initialEndTimeIndex && !hasInitialized.current) { + setSelectedEndTimeIndex(initialEndTimeIndex); + setEndTimeIndex(initialEndTimeIndex); + booking.setSelectedEndIndex(initialEndTimeIndex); + hasInitialized.current = true; + } + }, [initialEndTimeIndex, setEndTimeIndex, booking]); + + // Generate end time options based on available hours + const endTimeOptions = []; + const disabledOptions = {}; + + // Always show all possible options up to the original available hours, not limited by current selection + const maxOptions = Math.min(originalHoursAvailable.current, 8); + + for (let i = 1; i <= maxOptions; i++) { + const endTimeIndex = startTimeIndex + i; + const endTime = getTimeFromIndex(endTimeIndex); + const durationLabel = i === 1 ? "30 min" : + i === 2 ? "1 h" : + i === 3 ? "1.5 h" : + i === 4 ? "2 h" : + i === 5 ? "2.5 h" : + i === 6 ? "3 h" : + i === 7 ? "3.5 h" : + i === 8 ? "4 h" : `${i * 0.5} h`; + + endTimeOptions.push({ + value: endTimeIndex, + label: `${endTime} · ${durationLabel}` + }); + + disabledOptions[endTimeIndex] = false; // All available options are enabled + } + + function handleChange(event) { + const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedEndTimeIndex(endTimeValue); + + if (endTimeValue !== null) { + setEndTimeIndex(endTimeValue); + booking.setSelectedEndIndex(endTimeValue); + // Update the selected booking length in context so it doesn't interfere + const newLength = endTimeValue - startTimeIndex; + booking.setSelectedBookingLength && booking.setSelectedBookingLength(newLength); + } else { + // Reset to default state when placeholder is selected + setEndTimeIndex(startTimeIndex); + booking.setSelectedEndIndex(null); + booking.setSelectedBookingLength && booking.setSelectedBookingLength(0); + } + } + + // Check if user has selected an end time (including pre-selected) + const hasSelectedEndTime = selectedEndTimeIndex !== null; + + const handleNavigateToConfirmation = () => { + if (hasSelectedEndTime) { + // Navigate directly to booking confirmation instead of details + navigate('/booking-confirmation', { + state: { + selectedDate: booking.selectedDate, + selectedStartIndex: booking.selectedStartIndex, + selectedEndIndex: booking.selectedEndIndex, + assignedRoom: booking.assignedRoom, + title: booking.title, + participants: booking.participants + } + }); + } + }; + + return ( +
+ {/* Header */} +
+ {/* Time Selection - No Label */} +
+
+ +
+
+ + {/* Title Field - Compact, No Label */} +
+ +
+ + {/* Participants Field - Compact, No Label */} +
+ +
+
+ +
+ + {/* Actions */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/forms/BookingLengthField.jsx b/my-app/src/components/forms/BookingLengthField.jsx index faf19d4..9b80798 100644 --- a/my-app/src/components/forms/BookingLengthField.jsx +++ b/my-app/src/components/forms/BookingLengthField.jsx @@ -4,7 +4,7 @@ import { BOOKING_LENGTHS } from '../../constants/bookingConstants'; import { useBookingContext } from '../../context/BookingContext'; import styles from './BookingLengthField.module.css'; -export function BookingLengthField() { +export function BookingLengthField({ clean = false }) { const booking = useBookingContext(); return ( @@ -19,6 +19,7 @@ export function BookingLengthField() { value: 0 }} disabledOptions={booking.disabledOptions} + clean={clean} />
); diff --git a/my-app/src/components/forms/BookingTitleField.jsx b/my-app/src/components/forms/BookingTitleField.jsx index 2c517bf..efc1e3c 100644 --- a/my-app/src/components/forms/BookingTitleField.jsx +++ b/my-app/src/components/forms/BookingTitleField.jsx @@ -3,18 +3,20 @@ import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; import styles from './BookingTitleField.module.css'; -export function BookingTitleField({ compact = false }) { +export function BookingTitleField({ compact = false, hideLabel = false }) { const booking = useBookingContext(); const { getDefaultBookingTitle } = useSettingsContext(); return ( <> -

Titel på bokning

+ {!hideLabel && ( +

Titel på bokning

+ )} booking.setTitle(event.target.value)} - placeholder={getDefaultBookingTitle()} + placeholder={hideLabel ? "Titel på bokning" : getDefaultBookingTitle()} className={compact ? styles.compactTextInput : styles.textInput} /> diff --git a/my-app/src/components/forms/ParticipantsSelector.jsx b/my-app/src/components/forms/ParticipantsSelector.jsx index 9c2b5f3..fdcd2a5 100644 --- a/my-app/src/components/forms/ParticipantsSelector.jsx +++ b/my-app/src/components/forms/ParticipantsSelector.jsx @@ -4,7 +4,7 @@ import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; import styles from './ParticipantsSelector.module.css'; -export function ParticipantsSelector({ compact = false }) { +export function ParticipantsSelector({ compact = false, hideLabel = false }) { const booking = useBookingContext(); const { getCurrentUser } = useSettingsContext(); const [searchTerm, setSearchTerm] = useState(''); @@ -169,7 +169,9 @@ export function ParticipantsSelector({ compact = false }) { return (
-

Deltagare

+ {!hideLabel && ( +

Deltagare

+ )} {/* Search Input */}
@@ -182,7 +184,7 @@ export function ParticipantsSelector({ compact = false }) { onClick={handleInputClick} onBlur={handleInputBlur} onKeyDown={handleKeyDown} - placeholder="Search for participants..." + placeholder={hideLabel ? "Deltagare..." : "Sök deltagare..."} className={compact ? styles.compactSearchInput : styles.searchInput} role="combobox" aria-expanded={isDropdownOpen} diff --git a/my-app/src/components/forms/ParticipantsSelector.module.css b/my-app/src/components/forms/ParticipantsSelector.module.css index 796be70..6a6257c 100644 --- a/my-app/src/components/forms/ParticipantsSelector.module.css +++ b/my-app/src/components/forms/ParticipantsSelector.module.css @@ -15,6 +15,7 @@ } .selectedParticipants { + margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem; @@ -304,6 +305,7 @@ font-family: inherit; transition: border-color 0.2s ease; box-sizing: border-box; + margin-bottom: 0.4rem; } .compactSearchInput:focus { @@ -312,3 +314,4 @@ border-color: var(--color-primary); } + diff --git a/my-app/src/components/forms/RoomSelectionField.jsx b/my-app/src/components/forms/RoomSelectionField.jsx index 30923b9..8ab3ddc 100644 --- a/my-app/src/components/forms/RoomSelectionField.jsx +++ b/my-app/src/components/forms/RoomSelectionField.jsx @@ -4,7 +4,7 @@ import { useBookingContext } from '../../context/BookingContext'; import { useSettingsContext } from '../../context/SettingsContext'; import styles from './RoomSelectionField.module.css'; -export function RoomSelectionField() { +export function RoomSelectionField({ clean = false }) { const booking = useBookingContext(); const { settings } = useSettingsContext(); @@ -27,6 +27,7 @@ export function RoomSelectionField() { label: "Alla rum", value: "allRooms" }} + clean={clean} />
); diff --git a/my-app/src/components/layout/ThemeToggle.module.css b/my-app/src/components/layout/ThemeToggle.module.css index b6f608c..f8334eb 100644 --- a/my-app/src/components/layout/ThemeToggle.module.css +++ b/my-app/src/components/layout/ThemeToggle.module.css @@ -1,11 +1,11 @@ .toggleButton { - background-color: var(--bg-secondary); - border: 1px solid var(--border-light); + background-color: var(--bg-header-button); + border: 1px solid var(--header-button-border); cursor: pointer; padding: 0; border-radius: 50%; transition: var(--transition-fast); - color: var(--text-primary); + color: var(white); font-size: 1rem; display: flex; align-items: center; @@ -17,10 +17,8 @@ } .toggleButton:hover { - background-color: var(--bg-muted); - border-color: var(--border-medium); - box-shadow: var(--shadow-md); - transform: translateY(-1px); + background-color: var(--bg-header-button-hover); + border-color: var(--header-button-hover-border); } .toggleButton:active { @@ -29,6 +27,10 @@ } .toggleButton:focus { + outline: none; +} + +.toggleButton:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } \ No newline at end of file diff --git a/my-app/src/components/ui/Card.module.css b/my-app/src/components/ui/Card.module.css index e4c5400..cfba7e8 100644 --- a/my-app/src/components/ui/Card.module.css +++ b/my-app/src/components/ui/Card.module.css @@ -2,26 +2,25 @@ display: flex; flex-direction: column; gap: 1.2rem; - /*background: var(--bg-secondary);*/ - background: #151516; - border: 1px solid #2f2e2d; - /*border-radius: 8px;*/ + background: var(--bg-primary); + border: 1px solid #E0E0E0; overflow: hidden; cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + /*box-shadow: 0 2px 8px rgba(104, 104, 104, 0.15);*/ + box-shadow: 0 0 #0000, 0 0 #0000, 0px 4px 12px 0px rgba(0,0,0,0.12); text-decoration: none; - color: inherit; - aspect-ratio: 1 / 1; + color: var(--text-primary); + /*aspect-ratio: 1 / 1;*/ padding: 1rem; + flex: 1; } @media (hover: hover) { .card:hover { - border-color: #4a4a4a; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); - background-color: rgb(23, 23, 25); - transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); + border-color: #CECECE; + box-shadow: 0 0 #0000, 0 0 #0000, 0px 12px 20px 0px rgba(10,18,36,0.10); + transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1); } .card:hover .image { @@ -31,23 +30,30 @@ .card:hover .imageContainer::after { opacity: 1; } + + .actionButton:hover { + background: #F5F5F5; + border-color: #D7D7D7; + border-color: #CECECE; + } + + .card:active .image { + filter: grayscale(1); + } + + .card:active .imageContainer::after { + opacity: 1; + } + } -.card:active { - border-color: #4a4a4a; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); - background-color: rgb(23, 23, 25); -} - -.card:active .image { - filter: grayscale(1); -} - -.card:active .imageContainer::after { - opacity: 1; -} - +.card:active, .card:focus { + /*background-color: #FBFBFB;*/ +} + + +.card:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } @@ -98,7 +104,7 @@ margin: 0; font-size: 20px; font-weight: 400; - color: white; + color: #262626; line-height: 1.3; } @@ -106,15 +112,15 @@ margin: 0 0 16px 0; font-size: 14px; font-weight: 400; - color: #b0b0b0; + color: #5F5F5F; line-height: 1.4; } .actionButton { background: transparent; - border: 1px solid #504f4c; + border: 1px solid #E0E0E0; /*border-radius: 4px;*/ - color: white; + color: #111111; padding: 12px 16px; font-size: 14px; font-weight: 400; @@ -126,12 +132,65 @@ height: fit-content; } -.actionButton:hover { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.8); +.actionButton:focus{ + background-color: #E8E8E8; } -.actionButton:focus { - outline: 1px solid white; +.actionButton:active { + background: #F5F5F5; + border-color: #D7D7D7; + border-color: #CECECE; +} + +.actionButton:focus-visible { + outline: 2px solid var(--color-primary); outline-offset: 2px; + background-color: inherit; +} + + +/* DARK MODE STYLES */ +[data-theme="dark"] { + .card { + background: #151516; + border: 1px solid #2f2e2d; + box-shadow: none; + } + + .header { + color: #F1F1F1; + } + + .subheader { + color: #A7A7A7; + } + + .actionButton { + color: #F1F1F1; + border-color: #444341; + } + + .actionButton:active { + background: #202022; + border-color: #696763; + } + + + @media (hover: hover) { + .card:hover { + border-color: #4a4a4a; + /*box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);*/ + /*background-color: rgb(23, 23, 25);*/ + transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); + } + + .actionButton:hover { + background: #202022; + border-color: #696763; + } + + .actionButton:focus { + background-color: #262628; + } + } } \ No newline at end of file diff --git a/my-app/src/components/ui/Chip.jsx b/my-app/src/components/ui/Chip.jsx new file mode 100644 index 0000000..80e2310 --- /dev/null +++ b/my-app/src/components/ui/Chip.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import styles from './Chip.module.css'; + +export function Chip({ + children, + onClick, + href, + variant = 'default', + size = 'medium', + className = '', + ...props +}) { + const baseClasses = `${styles.chip} ${styles[variant]} ${styles[size]} ${className}`; + + if (href) { + return ( + + {children} + + ); + } + + if (onClick) { + return ( + + ); + } + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/ui/Chip.module.css b/my-app/src/components/ui/Chip.module.css new file mode 100644 index 0000000..333838b --- /dev/null +++ b/my-app/src/components/ui/Chip.module.css @@ -0,0 +1,88 @@ +.chip { + display: inline-flex; + align-items: center; + border-radius: 12px; + font-weight: 500; + text-decoration: none; + border: none; + cursor: default; + transition: all 0.2s ease; + white-space: nowrap; + width: fit-content; +} + +/* Variants */ +.default { + background-color: #f3f4f6; + color: #374151; +} + +.primary { + background-color: #3b82f6; + color: white; +} + +.success { + background-color: #10b981; + color: white; +} + +.warning { + background-color: #f59e0b; + color: white; +} + +.error { + background-color: #ef4444; + color: white; +} + +.room { + background-color: #e0e7ff; + color: #3730a3; + border: 1px solid #c7d2fe; +} + +/* Sizes */ +.small { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.medium { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +.large { + padding: 0.5rem 1rem; + font-size: 1rem; +} + +/* Interactive states */ +.link, +.button { + cursor: pointer; +} + +.link:hover, +.button:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.link:active, +.button:active { + transform: translateY(0); +} + +.room.link:hover, +.room.button:hover { + background-color: #c7d2fe; + border-color: #a5b4fc; +} + +/* Remove button styling */ +.button { + font-family: inherit; +} \ No newline at end of file diff --git a/my-app/src/components/ui/Dropdown.jsx b/my-app/src/components/ui/Dropdown.jsx index 04fa872..c632683 100644 --- a/my-app/src/components/ui/Dropdown.jsx +++ b/my-app/src/components/ui/Dropdown.jsx @@ -3,13 +3,13 @@ import styles from "./Dropdown.module.css"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' -const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "Select an option"}, disabledOptions = false, className }) => { +const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "Select an option"}, disabledOptions = false, className, clean = false }) => { return (
Current: {settings.bookingFormType === 'inline' ? 'Inline Form' : settings.bookingFormType === 'modal' ? 'Modal Popup' : - 'Inline Modal'} + settings.bookingFormType === 'inline-modal' ? 'Inline Modal' : + settings.bookingFormType === 'inline-modal-extended' ? 'Inline Modal Extended' : + settings.bookingFormType === 'inline-modal-extended-no-labels' ? 'Inline Modal Extended No Labels' : + 'Unknown'}
Inline Form: All fields in one form
Modal Popup: Time selection in popup, then details page
- Inline Modal: Time selection inline, then details page + Inline Modal: Time selection inline, then details page
+ Inline Modal Extended: Like hybrid, plus title and participants
+ Inline Modal Extended No Labels: Like extended hybrid, but without field labels +
+
+ +
+ +
+ updateSettings({ showFiltersAlways: e.target.checked })} + className={styles.toggle} + /> + + {settings.showFiltersAlways ? 'Always Show Dropdowns' : 'Show Filter Button'} + +
+
+ Always Show Dropdowns: Room and duration filters are always visible
+ Show Filter Button: Filters are hidden behind a collapsible button
diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index e3dcf41..54ac102 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -66,44 +66,59 @@ export function NewBooking({ addBooking }) {
- {/* Filter Button */} -
- - - {/* Collapsible Filter Content */} - {showFilters && ( -
-
- - -
- {hasActiveFilters && ( -
- + {/* Filter Section */} +
+
+ {settings.showFiltersAlways ? ( + /* Always-visible filters */ +
+
+ +
- )} -
- )} +
+ ) : ( + /* Toggle button with collapsible filters */ + <> + + + {/* Collapsible Filter Content */} + {showFilters && ( +
+
+ + +
+ {hasActiveFilters && ( +
+ +
+ )} +
+ )} + + )} +
+

+ Välj starttid +

-

- Lediga tider -

- +
diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index 60f7b4f..bfe1c80 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -1,6 +1,6 @@ .pageContainer { padding: var(--container-padding); - background-color: var(--bg-tertiary); + background-color: var(--bg-primary); color: var(--text-primary); min-height: 100vh; } @@ -14,18 +14,34 @@ padding-bottom: 4rem; } +.elementHeading { + font-size: 1.4rem; + margin: 0; + margin-bottom: 0.5rem; + font-weight: 400; +} + .formContainer h2 { font-weight: 529; } .bookingTimesContainer { margin-top: 2rem; - padding: 2rem; border-radius: 0.3rem; outline: 1px solid var(--border-light); display: flex; flex-direction: column; align-items: center; + background-color: var(--bg-secondary); + padding-bottom: 2rem; +} + +.headerAndFilter { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } @@ -148,7 +164,6 @@ /* Filter Section Styles */ .filtersSection { - width: 100%; margin-top: 1rem; margin-bottom: 1.5rem; display: flex; @@ -222,6 +237,15 @@ animation: slideDown 0.2s ease-out; } +.filtersContentClean { + width: fit-content; + max-width: 600px; + padding: 1rem; + background: transparent; + border: none; + border-radius: 0; +} + .filtersRow { display: flex; gap: 1rem; diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 6ca65c8..a6599d5 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -32,7 +32,11 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
)} -

Lokalbokning

+
+

Lokalbokning

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

Ny bokning

@@ -45,23 +49,24 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o

-

Mina bokingar

- -
+
+

Mina bokingar

+ +
); } \ No newline at end of file diff --git a/my-app/src/pages/RoomBooking.module.css b/my-app/src/pages/RoomBooking.module.css index 179e3b4..a7a396d 100644 --- a/my-app/src/pages/RoomBooking.module.css +++ b/my-app/src/pages/RoomBooking.module.css @@ -1,15 +1,41 @@ .pageContainer { - color: var(--text-primary); - background-color: var(--bg-primary); - padding: var(--container-padding); + max-width: 1200px; + margin: 0 auto; + padding: var(--spacing-2xl); min-height: 100vh; } +.header { + margin-bottom: var(--spacing-2xl); + padding-bottom: var(--spacing-xl); + border-bottom: 1px solid var(--border-light); +} + +.badge { + display: inline-block; + background-color: var(--color-primary); + color: var(--color-white); + padding: var(--spacing-xs) var(--spacing-md); + border-radius: var(--border-radius-md); + font-size: 0.875rem; + font-weight: var(--font-weight-semibold); + margin-bottom: var(--spacing-md); + text-transform: uppercase; + letter-spacing: 0.5px; +} + .pageHeading { - font-size: var(--font-size-6xl); - font-weight: var(--font-weight-medium); - font-family: var(--font-header); color: var(--text-primary); + margin: 0 0 var(--spacing-md) 0; + font-size: 2.5rem; + font-weight: var(--font-weight-bold); + line-height: 1.2; +} + +.subtitle { + color: var(--text-secondary); + font-size: 1.1rem; + font-weight: var(--font-weight-medium); } .sectionHeading { @@ -25,6 +51,19 @@ gap: 2rem; } +@media (min-width: 520px) { + .roomCategoryCards { + flex-direction: row; + flex-wrap: wrap; + } + + .roomCategoryCards > * { + flex: 1 1 0; + min-width: 200px; + max-width: 400px; + } +} + .sectionDivider { border: none; margin-top: 4rem; diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index 48d02c1..f22e73f 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -12,9 +12,15 @@ padding: var(--spacing-md); border: 1px solid var(--border-light); position: sticky; + width: 100%; top: 1rem; + z-index: 10; + display: flex; + flex-direction: row; + justify-content: center; .react-aria-Group { + height: fit-content; display: flex; width: fit-content; align-items: center; @@ -22,6 +28,7 @@ flex-direction: row; justify-content: space-between; gap: 2rem; + } .react-aria-Button { @@ -165,6 +172,6 @@ @media (max-width: 1024px) { .react-aria-DatePicker { - top: 5rem; + top: 4rem; } } \ No newline at end of file diff --git a/my-app/src/styles/variables.css b/my-app/src/styles/variables.css index 698c35b..b2c8ca7 100644 --- a/my-app/src/styles/variables.css +++ b/my-app/src/styles/variables.css @@ -36,6 +36,13 @@ --bg-secondary: #f8f8f8; --bg-tertiary: #fafafa; --bg-muted: #f0f0f0; + + + /* Header button */ + --bg-header-button: #00366E; + --bg-header-button-hover: #134E8B; + --header-button-border: #295481; + --header-button-hover-border: #688EB5; /* iPhone/iPod Style Backgrounds */ --bg-iphone-gradient: linear-gradient(to bottom, #c5ccd4, #92a5b8); @@ -432,10 +439,16 @@ /* Background Colors */ /*--bg-primary: #1a1a1a;*/ - --bg-primary: #0F0F0F; + --bg-primary: #1a1919; --bg-secondary: #21211F; --bg-tertiary: #333; --bg-muted: #3a3a3a; + + /* Header button */ + --bg-header-button: #1D3854; + --bg-header-button-hover: #28425D; + --header-button-border: #2F4861; + --header-button-hover-border: #6E859E; /* iPhone/iPod Style Backgrounds - dark variants */ --bg-iphone-gradient: linear-gradient(to bottom, #2a2a2a, #1a1a1a);