eriks-booking-variant #6
@ -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 />} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
329
my-app/src/components/booking/InlineModalExtendedBookingForm.jsx
Normal file
329
my-app/src/components/booking/InlineModalExtendedBookingForm.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 på bokning</h3>
|
||||
{!hideLabel && (
|
||||
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel på 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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
my-app/src/components/ui/Chip.jsx
Normal file
44
my-app/src/components/ui/Chip.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
88
my-app/src/components/ui/Chip.module.css
Normal file
88
my-app/src/components/ui/Chip.module.css
Normal 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;
|
||||
}
|
||||
@ -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}>
|
||||
|
||||
@ -29,3 +29,9 @@
|
||||
font-size: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.clean {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
122
my-app/src/pages/BookingConfirmation.jsx
Normal file
122
my-app/src/pages/BookingConfirmation.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
314
my-app/src/pages/BookingConfirmation.module.css
Normal file
314
my-app/src/pages/BookingConfirmation.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user