eriks-booking-variant #6

Merged
jare2473 merged 13 commits from eriks-booking-variant into main 2025-09-22 11:16:13 +02:00
29 changed files with 1542 additions and 152 deletions

View File

@ -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 = () => {
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} />
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
<Route path="booking-details" element={<BookingDetails addBooking={addBooking} />} />
<Route path="booking-confirmation" element={<BookingConfirmation addBooking={addBooking} />} />
<Route path="course-schedule" element={<CourseSchedule />} />
<Route path="course-schedule/:courseId" element={<CourseScheduleView />} />
<Route path="booking-settings" element={<BookingSettings />} />

View File

@ -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
/>
)}
<div className={styles.bookingsContainer}>
{bookings.length > 0 ? (
{sortedBookings.length > 0 ? (
<>
{displayedBookings.map((booking, index) => (
{sortedBookings.map((booking, index) => (
<BookingCard
key={index}
booking={booking}
@ -80,24 +83,6 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
onBookingDelete={onBookingDelete}
/>
))}
{hasMoreBookings && (
<button
className={styles.showMoreButton}
onClick={() => setShowAll(!showAll)}
>
{showAll ? (
<>
<span>Visa färre</span>
<span></span>
</>
) : (
<>
<span>Visa {bookings.length - INITIAL_DISPLAY_COUNT} fler</span>
<span></span>
</>
)}
</button>
)}
</>
) : (
<p className={styles.message}>Du har inga bokningar just nu</p>

View File

@ -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);
}

View File

@ -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 (
<div
ref={confirmationRef}
className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}
>
<div className={styles.formHeader}>
<div style={{ textAlign: 'center', padding: '1rem' }}>
<h3 style={{ margin: '0 0 0.5rem 0', color: '#28a745' }}>Bokning sparad!</h3>
<p style={{ margin: '0', fontSize: '0.9rem', color: '#666' }}>
{booking.title ? `${booking.title}` : `${getDefaultBookingTitle()}`}
</p>
{(() => {
// 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 (
<div style={{ marginTop: '0.5rem' }}>
<p style={{ margin: '0', fontSize: '0.85rem', color: '#888' }}>
{dateStr} {startTime} - {endTime}
</p>
<div style={{ margin: '0.5rem 0 0 0', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.85rem', color: '#888' }}>Rum:</span>
<Chip variant="room" size="medium" href={`#room-${roomName}`}>
{roomName}
</Chip>
</div>
{allParticipants.length > 0 && (
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.85rem', color: '#888' }}>
Deltagare: {allParticipants.map(p => p.name).join(', ')}
</p>
)}
</div>
);
})()}
</div>
</div>
<hr className={extendedStyles.divider} />
<div className={styles.confirmationActions}>
<Button
className={styles.saveButton}
onPress={() => {
navigate('/');
// Scroll to bookings after navigation
setTimeout(() => {
const bookingsElement = document.getElementById('bookings');
if (bookingsElement) {
bookingsElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}, 100);
}}
style={{
height: '2.75rem',
minHeight: '2.75rem',
flex: 'none',
width: '100%'
}}
>
Se bokningar
</Button>
<Button
className={styles.cancelButton}
onPress={onClose}
style={{
height: '2.75rem',
minHeight: '2.75rem',
flex: 'none',
width: '100%'
}}
>
Stäng
</Button>
</div>
</div>
);
}
return (
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
{/* Header */}
<div className={styles.formHeader}>
{/* Time Selection */}
<div className={extendedStyles.section}>
<div className={styles.formField}>
<label className={styles.formLabel}>Sluttid</label>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
onChange={handleChange}
value={selectedEndTimeIndex || ""}
placeholder={!initialEndTimeIndex ? {
value: "",
label: "Välj sluttid"
} : null}
className={styles.endTimeDropdown}
/>
</div>
</div>
{/* Title Field - Compact */}
<div className={extendedStyles.section}>
<BookingTitleField compact={true} />
</div>
{/* Participants Field - Compact */}
<div className={extendedStyles.section}>
<ParticipantsSelector compact={true} />
</div>
</div>
<hr className={extendedStyles.divider} />
{/* Actions */}
<div className={styles.formActions}>
<Button className={styles.cancelButton} onPress={onClose}>
Avbryt
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onPress={hasSelectedEndTime ? handleSave : undefined}
isDisabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}
</Button>
</div>
</div>
);
}

View File

@ -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;
}

View File

@ -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 (
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
{/* Header */}
<div className={styles.formHeader}>
{/* Time Selection - No Label */}
<div className={extendedStyles.section}>
<div className={styles.formField}>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
onChange={handleChange}
value={selectedEndTimeIndex || ""}
placeholder={!initialEndTimeIndex ? {
value: "",
label: "Välj sluttid"
} : null}
className={styles.endTimeDropdown}
/>
</div>
</div>
{/* Title Field - Compact, No Label */}
<div className={extendedStyles.section}>
<BookingTitleField compact={true} hideLabel={true} />
</div>
{/* Participants Field - Compact, No Label */}
<div className={extendedStyles.section}>
<ParticipantsSelector compact={true} hideLabel={true} />
</div>
</div>
<hr className={extendedStyles.divider} />
{/* Actions */}
<div className={styles.formActions}>
<Button className={styles.cancelButton} onPress={onClose}>
Avbryt
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onPress={hasSelectedEndTime ? handleNavigateToConfirmation : undefined}
isDisabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}
</Button>
</div>
</div>
);
}

View File

@ -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}
/>
</div>
);

View File

@ -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 (
<>
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel bokning</h3>
{!hideLabel && (
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel bokning</h3>
)}
<input
type="text"
value={booking.title}
onChange={(event) => booking.setTitle(event.target.value)}
placeholder={getDefaultBookingTitle()}
placeholder={hideLabel ? "Titel på bokning" : getDefaultBookingTitle()}
className={compact ? styles.compactTextInput : styles.textInput}
/>
</>

View File

@ -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 (
<div className={compact ? styles.compactContainer : styles.container}>
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Deltagare</h3>
{!hideLabel && (
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Deltagare</h3>
)}
{/* Search Input */}
<div className={styles.searchContainer} ref={dropdownRef}>
@ -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}

View File

@ -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);
}

View File

@ -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}
/>
</div>
);

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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 (
<a
href={href}
className={`${baseClasses} ${styles.link}`}
{...props}
>
{children}
</a>
);
}
if (onClick) {
return (
<button
onClick={onClick}
className={`${baseClasses} ${styles.button}`}
{...props}
>
{children}
</button>
);
}
return (
<span className={baseClasses} {...props}>
{children}
</span>
);
}

View File

@ -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;
}

View File

@ -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 (
<div className={`${styles.dropdownWrapper} ${className || ''}`}>
<select
value={value}
onChange={onChange}
className={styles.select}
className={`${styles.select} ${clean ? styles.clean : ''}`}
>
{placeholder && (
<option value={placeholder.value} disabled={false} hidden={false}>

View File

@ -29,3 +29,9 @@
font-size: 0.8rem;
z-index: 1;
}
.clean {
border: none;
background: transparent;
padding-left: 0;
}

View File

@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom';
import TimeCard from './TimeCard';
import { InlineBookingForm } from '../booking/InlineBookingForm';
import { InlineModalBookingForm } from '../booking/InlineModalBookingForm';
import { InlineModalExtendedBookingForm } from '../booking/InlineModalExtendedBookingForm';
import { InlineModalExtendedBookingFormNoLabels } from '../booking/InlineModalExtendedBookingFormNoLabels';
import { BookingModal } from '../booking/BookingModal';
import styles from './TimeCardContainer.module.css';
import modalStyles from '../booking/BookingModal.module.css';
@ -11,7 +13,7 @@ import { useSettingsContext } from '../../context/SettingsContext';
const SLOT_GROUPING_SIZE = 8;
export function TimeCardContainer() {
export function TimeCardContainer({ addBooking }) {
const navigate = useNavigate();
const booking = useBookingContext();
const { settings } = useSettingsContext();
@ -19,6 +21,8 @@ export function TimeCardContainer() {
// Check booking form type
const useInlineForm = settings.bookingFormType === 'inline';
const useInlineModal = settings.bookingFormType === 'inline-modal';
const useInlineModalExtended = settings.bookingFormType === 'inline-modal-extended';
const useInlineModalExtendedNoLabels = settings.bookingFormType === 'inline-modal-extended-no-labels';
const useModal = settings.bookingFormType === 'modal';
const handleNavigateToDetails = () => {
@ -143,7 +147,7 @@ export function TimeCardContainer() {
// Add inline booking form after the pair that contains the selected time card
// Cards are laid out in pairs: (0,1), (2,3), (4,5), etc.
if ((useInlineForm || useInlineModal) && booking.selectedStartIndex !== null) {
if ((useInlineForm || useInlineModal || useInlineModalExtended || useInlineModalExtendedNoLabels) && booking.selectedStartIndex !== null) {
const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2;
const selectedPairEnd = selectedPairStart + 1;
@ -174,6 +178,33 @@ export function TimeCardContainer() {
arrowPointsLeft={isLeftCard}
/>
);
} else if (useInlineModalExtended) {
elements.push(
<InlineModalExtendedBookingForm
key={`form-${slotIndex}-${booking.selectedStartIndex}`}
startTimeIndex={booking.selectedStartIndex}
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
endTimeIndex={booking.selectedEndIndex}
setEndTimeIndex={booking.setSelectedEndIndex}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
addBooking={addBooking}
arrowPointsLeft={isLeftCard}
/>
);
} else if (useInlineModalExtendedNoLabels) {
elements.push(
<InlineModalExtendedBookingFormNoLabels
key={`form-${slotIndex}`}
startTimeIndex={booking.selectedStartIndex}
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
endTimeIndex={booking.selectedEndIndex}
setEndTimeIndex={booking.setSelectedEndIndex}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
arrowPointsLeft={isLeftCard}
/>
);
}
}
}

View File

@ -32,6 +32,7 @@ export const SettingsProvider = ({ children }) => {
showBookingConfirmationBanner: false,
showBookingDeleteBanner: false,
bookingFormType: 'inline', // 'modal' or 'inline'
showFiltersAlways: false, // Show filter dropdowns always or behind toggle button
// Then override with saved values
...parsed,
// Convert date strings back to DateValue objects
@ -67,6 +68,8 @@ export const SettingsProvider = ({ children }) => {
showBookingDeleteBanner: false,
// Booking form type
bookingFormType: 'inline', // 'modal' or 'inline'
// Filter display mode
showFiltersAlways: false, // Show filter dropdowns always or behind toggle button
};
});
@ -111,6 +114,7 @@ export const SettingsProvider = ({ children }) => {
showBookingConfirmationBanner: false,
showBookingDeleteBanner: false,
bookingFormType: 'inline',
showFiltersAlways: false,
});
localStorage.removeItem('calendarSettings');
};

View File

@ -0,0 +1,122 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import styles from './BookingConfirmation.module.css';
import { useBookingContext } from '../context/BookingContext';
import { BookingProvider } from '../context/BookingContext';
import { useSettingsContext } from '../context/SettingsContext';
import { useBookingState } from '../hooks/useBookingState';
import { convertDateObjectToString, formatBookingDate, getTimeFromIndex } from '../helpers';
export function BookingConfirmation({ addBooking }) {
const navigate = useNavigate();
const location = useLocation();
const { getEffectiveToday, getCurrentUser } = useSettingsContext();
const booking = useBookingState(addBooking, getEffectiveToday());
useEffect(() => {
window.scrollTo(0, 0);
// Automatically save the booking when the page loads
if (location.state && booking) {
setTimeout(() => {
booking.handleSave();
}, 100); // Small delay to ensure state is populated
}
}, [location.state]);
// Populate booking state from navigation state if available
useEffect(() => {
const navigationState = location.state;
if (navigationState) {
// Update booking state with navigation data
if (navigationState.selectedDate) booking.setSelectedDate(navigationState.selectedDate);
if (navigationState.selectedStartIndex !== undefined) booking.setSelectedStartIndex(navigationState.selectedStartIndex);
if (navigationState.selectedEndIndex !== undefined) booking.setSelectedEndIndex(navigationState.selectedEndIndex);
if (navigationState.assignedRoom) booking.setAssignedRoom(navigationState.assignedRoom);
if (navigationState.title) booking.setTitle(navigationState.title);
if (navigationState.participants) booking.setParticipants(navigationState.participants);
} else if (!booking.selectedDate || !booking.selectedStartIndex || !booking.selectedEndIndex) {
// Redirect back if no booking data from navigation or state
navigate('/new-booking');
}
}, [location.state, navigate]);
const handleBackToBookings = () => {
navigate('/');
};
const handleNewBooking = () => {
navigate('/new-booking');
};
// Get current user and all participants
const currentUser = getCurrentUser();
const allParticipants = [currentUser, ...(booking.participants || [])];
return (
<BookingProvider value={booking}>
<div className={styles.container}>
{/* Combined Confirmation and Room Section */}
<div className={styles.combinedSection}>
{/* Booking Confirmation */}
<div className={styles.bookingConfirmed}>
<div className={styles.bannerContent}>
<div className={styles.successIcon}></div>
<div className={styles.bannerText}>
<h1 className={styles.successTitle}>
Booking confirmed: <span className={styles.bookingTitle}>"{booking.title}"</span>
</h1>
<div className={styles.bookingDetails}>
{booking.assignedRoom || 'G5:12'} {booking.selectedDate ? formatBookingDate(booking.selectedDate) : 'Välj datum'} {getTimeFromIndex(booking.selectedStartIndex)}-{getTimeFromIndex(booking.selectedEndIndex)} {allParticipants.map(p => p.name).join(', ')}
</div>
</div>
</div>
</div>
{/* Room Visualization */}
<div className={styles.roomVisualization}>
<div className={styles.roomArea}></div>
<div className={styles.roomInfo}>
<div className={styles.roomHeader}>
<span>Lokal: {booking.assignedRoom || 'G5:12'}</span>
</div>
<div className={styles.roomDetails}>
<div className={styles.roomDetailItem}>
<span className={styles.roomDetailLabel}>5 platser</span>
</div>
<div className={styles.roomDetailItem}>
<span className={styles.roomDetailLabel}>Plats:</span>
<span className={styles.roomDetailValue}>Röda avdelningen</span>
</div>
<div className={styles.roomDetailItem}>
<span className={styles.roomDetailLabel}>Utrustning:</span>
<span className={styles.roomDetailValue}>Whiteboard, TV, Tangentbord, Mus</span>
</div>
<div className={styles.roomDetailItem}>
<span className={styles.roomDetailLabel}> Status:</span>
<span className={styles.roomDetailValue}>HDMI-kabel glappar</span>
</div>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={handleNewBooking}
>
Boka igen
</button>
<button
className={styles.primaryButton}
onClick={handleBackToBookings}
>
Visa mina bokningar
</button>
</div>
</div>
</BookingProvider>
);
}

View File

@ -0,0 +1,314 @@
.container {
margin: 0 auto;
padding: 2rem 1rem;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg-primary);
width: 100%;
box-sizing: border-box;
}
/* Combined Section Container */
.combinedSection {
margin: 2rem 0;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
width: 100%;
}
/* Combined Booking Confirmed Section - Banner-like */
.bookingConfirmed {
padding: var(--spacing-lg) var(--spacing-xl);
background: var(--notification-success-bg);
border: var(--border-width-thin) solid var(--notification-success-border);
border-bottom: none;
position: relative;
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
justify-content: center;
}
@keyframes shimmer {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
.successIcon {
width: 2rem;
height: 2rem;
background: var(--notification-success-icon-bg);
color: var(--notification-success-icon-text);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
flex-shrink: 0;
margin-right: var(--spacing-lg);
}
@keyframes successPulse {
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.bannerContent {
display: flex;
align-items: center;
gap: var(--spacing-lg);
width: 100%;
}
.bannerText {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
flex: 1;
min-width: 0;
}
.successTitle {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--notification-success-title);
margin: 0;
}
.bookingDetails {
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
color: var(--notification-success-details);
margin: 0;
line-height: 1.4;
}
.date {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--notification-success-title);
}
.time {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-medium);
color: var(--notification-success-details);
}
.room {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-primary);
background: var(--bg-primary);
padding: 0.75rem 1.5rem;
border-radius: 1rem;
border: 2px solid var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.bookingTitle {
font-weight: var(--font-weight-normal);
font-style: italic;
}
.participants {
font-size: var(--font-size-md);
color: var(--notification-success-details);
font-weight: var(--font-weight-medium);
line-height: 1.6;
max-width: 80%;
}
/* Action Buttons */
.actions {
display: flex;
gap: 1rem;
width: 100%;
max-width: 400px;
box-sizing: border-box;
}
.primaryButton {
flex: 2;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-hover));
color: white;
border: none;
border-radius: 0.75rem;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.primaryButton:active {
transform: translateY(0);
}
.secondaryButton {
flex: 1;
background: var(--bg-secondary);
color: var(--text-primary);
border: 2px solid var(--border-light);
border-radius: 0.75rem;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.secondaryButton:hover {
background: var(--bg-tertiary);
border-color: var(--border-medium);
}
.secondaryButton:active {
transform: translateY(1px);
}
/* Room Visualization - Connected to booking confirmation */
.combinedSection .roomVisualization {
margin-bottom: 0;
border-radius: 0;
overflow: hidden;
box-shadow: none;
width: 100%;
}
.roomArea {
height: 200px;
background-image: url('./grupprum.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.roomInfo {
border-top: 2px solid var(--su-sky);
background: var(--su-blue);
color: white;
padding: 1.5rem;
font-weight: 500;
}
.roomHeader {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1.25rem;
}
.roomDetails {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.roomDetailItem {
display: flex;
gap: 0.75rem;
align-items: flex-start;
font-size: 0.95rem;
line-height: 1.4;
}
.roomDetailItem:first-child {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.roomDetailItem:first-child .roomDetailLabel {
color: white;
}
.roomDetailLabel {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
min-width: fit-content;
font-size: inherit;
}
.roomDetailValue {
color: white;
font-weight: 400;
font-size: inherit;
}
.spacer {
height: 1rem;
}
/* Responsive */
@media (max-width: 640px) {
.container {
padding: 1rem 0.5rem;
}
.combinedSection {
margin: 1.5rem 0;
}
.bookingConfirmed {
padding: var(--spacing-md) var(--spacing-lg);
}
.bannerContent {
flex-direction: column;
text-align: center;
gap: var(--spacing-md);
}
.successIcon {
margin-right: 0;
margin-bottom: var(--spacing-sm);
}
.successTitle {
font-size: var(--font-size-lg);
}
.bookingDetails {
font-size: var(--font-size-sm);
}
.actions {
flex-direction: column;
}
.actions button {
flex: none;
}
}

View File

@ -47,6 +47,44 @@
text-transform: uppercase;
}
/* Booking confirmation summary styles */
.summaryHeading {
margin: 0;
color: var(--text-tertiary);
font-size: 0.8rem;
font-style: normal;
font-weight: 520;
line-height: normal;
margin-bottom: 0.2rem;
margin-top: 1.5rem;
}
.summaryValue {
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 0.5rem;
padding: 1rem;
color: var(--input-text);
font-size: 16px;
}
.participantsList {
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: 0.5rem;
padding: 1rem;
}
.participantItem {
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-light);
color: var(--input-text);
}
.participantItem:last-child {
border-bottom: none;
}
.bookingInfo {
margin-bottom: 2rem;
}

View File

@ -150,18 +150,50 @@ export function BookingSettings() {
<option value="inline">Inline Form (Complete)</option>
<option value="modal">Modal Popup (Classic)</option>
<option value="inline-modal">Inline Modal (Hybrid)</option>
<option value="inline-modal-extended">Inline Modal Extended (Hybrid+)</option>
<option value="inline-modal-extended-no-labels">Inline Modal Extended No Labels (Hybrid+ Clean)</option>
</select>
<div className={styles.currentStatus}>
Current: <strong>
{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'}
</strong>
</div>
<div className={styles.description} style={{marginTop: '0.5rem', fontSize: '0.85rem'}}>
<strong>Inline Form:</strong> All fields in one form<br/>
<strong>Modal Popup:</strong> Time selection in popup, then details page<br/>
<strong>Inline Modal:</strong> Time selection inline, then details page
<strong>Inline Modal:</strong> Time selection inline, then details page<br/>
<strong>Inline Modal Extended:</strong> Like hybrid, plus title and participants<br/>
<strong>Inline Modal Extended No Labels:</strong> Like extended hybrid, but without field labels
</div>
</div>
<div className={styles.setting}>
<label htmlFor="showFiltersAlways">
<strong>Filter Display Mode</strong>
<span className={styles.description}>
Choose whether filter dropdowns are always visible or hidden behind a toggle button
</span>
</label>
<div className={styles.toggleGroup}>
<input
id="showFiltersAlways"
type="checkbox"
checked={settings.showFiltersAlways}
onChange={(e) => updateSettings({ showFiltersAlways: e.target.checked })}
className={styles.toggle}
/>
<span className={styles.toggleStatus}>
{settings.showFiltersAlways ? 'Always Show Dropdowns' : 'Show Filter Button'}
</span>
</div>
<div className={styles.description} style={{marginTop: '0.5rem', fontSize: '0.85rem'}}>
<strong>Always Show Dropdowns:</strong> Room and duration filters are always visible<br/>
<strong>Show Filter Button:</strong> Filters are hidden behind a collapsible button
</div>
</div>
</div>

View File

@ -66,44 +66,59 @@ export function NewBooking({ addBooking }) {
<div className={styles.bookingTimesContainer}>
<BookingDatePicker />
{/* Filter Button */}
<div className={styles.filtersSection}>
<button
className={`${styles.filterButton} ${hasActiveFilters ? styles.activeFilter : ''}`}
onClick={() => setShowFilters(!showFilters)}
>
<span>{getFilterText()}</span>
<span className={`${styles.chevron} ${showFilters ? styles.chevronUp : styles.chevronDown}`}>
{showFilters ? '▲' : '▼'}
</span>
</button>
{/* Collapsible Filter Content */}
{showFilters && (
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
{hasActiveFilters && (
<div className={styles.resetSection}>
<button
className={styles.resetButton}
onClick={handleResetFilters}
>
Rensa filter
</button>
{/* Filter Section */}
<div className={styles.headerAndFilter}>
<div className={styles.filtersSection}>
{settings.showFiltersAlways ? (
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
)}
</div>
)}
</div>
) : (
/* Toggle button with collapsible filters */
<>
<button
className={`${styles.filterButton} ${hasActiveFilters ? styles.activeFilter : ''}`}
onClick={() => setShowFilters(!showFilters)}
>
<span>{getFilterText()}</span>
<span className={`${styles.chevron} ${showFilters ? styles.chevronUp : styles.chevronDown}`}>
{showFilters ? '▲' : '▼'}
</span>
</button>
{/* Collapsible Filter Content */}
{showFilters && (
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
{hasActiveFilters && (
<div className={styles.resetSection}>
<button
className={styles.resetButton}
onClick={handleResetFilters}
>
Rensa filter
</button>
</div>
)}
</div>
)}
</>
)}
</div>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
</div>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Lediga tider
</h3>
<div>
<TimeCardContainer />
<TimeCardContainer addBooking={addBooking} />
</div>
</div>
</main>

View File

@ -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;

View File

@ -32,7 +32,11 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
</div>
</div>
)}
<h1 className={styles.pageHeading}>Lokalbokning</h1>
<div className={styles.header}>
<h1 className={styles.pageHeading}>Lokalbokning</h1>
<div className={styles.subtitle}>Reservera lokaler för möten och studier</div>
</div>
<h2 className={styles.sectionHeading}>Ny bokning</h2>
<div className={styles.roomCategoryCards}>
@ -45,23 +49,24 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
</div>
<hr className={styles.sectionDivider} />
<h2 className={styles.sectionHeading}>Mina bokingar</h2>
<BookingsList
bookings={bookings}
handleEditBooking={handleEditBooking}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
showSuccessBanner={showSuccessBanner}
lastCreatedBooking={lastCreatedBooking}
onDismissBanner={onDismissBanner}
showDeleteBanner={showDeleteBanner}
lastDeletedBooking={lastDeletedBooking}
onDismissDeleteBanner={onDismissDeleteBanner}
showDevelopmentBanner={settings.showDevelopmentBanner}
showBookingConfirmationBanner={settings.showBookingConfirmationBanner}
showBookingDeleteBanner={settings.showBookingDeleteBanner}
/>
<hr className={styles.sectionDivider} />
<section id="bookings">
<h2 className={styles.sectionHeading}>Mina bokingar</h2>
<BookingsList
bookings={bookings}
handleEditBooking={handleEditBooking}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
showSuccessBanner={showSuccessBanner}
lastCreatedBooking={lastCreatedBooking}
onDismissBanner={onDismissBanner}
showDeleteBanner={showDeleteBanner}
lastDeletedBooking={lastDeletedBooking}
onDismissDeleteBanner={onDismissDeleteBanner}
showDevelopmentBanner={settings.showDevelopmentBanner}
showBookingConfirmationBanner={settings.showBookingConfirmationBanner}
showBookingDeleteBanner={settings.showBookingDeleteBanner}
/>
</section>
</div>
);
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);