new-modal #4
4
my-app/.gitignore
vendored
4
my-app/.gitignore
vendored
@ -25,3 +25,7 @@ dist-ssr
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# Font files
|
||||
public/caecilia/
|
||||
public/the-sans/
|
||||
|
||||
44
my-app/public/su-logo-white.svg
Normal file
44
my-app/public/su-logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,6 +5,7 @@ import { CalendarDate } from '@internationalized/date';
|
||||
import Layout from './Layout';
|
||||
import { RoomBooking } from './pages/RoomBooking';
|
||||
import { NewBooking } from './pages/NewBooking';
|
||||
import { BookingDetails } from './pages/BookingDetails';
|
||||
import { BookingSettings } from './pages/BookingSettings';
|
||||
import { CourseSchedule } from './pages/CourseSchedule';
|
||||
import { CourseScheduleView } from './pages/CourseScheduleView';
|
||||
@ -119,6 +120,7 @@ const AppRoutes = () => {
|
||||
<Route path="/" element={<Layout />}>
|
||||
<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="course-schedule" element={<CourseSchedule />} />
|
||||
<Route path="course-schedule/:courseId" element={<CourseScheduleView />} />
|
||||
<Route path="booking-settings" element={<BookingSettings />} />
|
||||
|
||||
@ -4,6 +4,8 @@ import { convertDateObjectToString, getTimeFromIndex } from '../../helpers';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import { useBookingContext } from '../../context/BookingContext';
|
||||
import { useSettingsContext } from '../../context/SettingsContext';
|
||||
import { BookingTitleField } from '../forms/BookingTitleField';
|
||||
import { ParticipantsSelector } from '../forms/ParticipantsSelector';
|
||||
import styles from './BookingModal.module.css';
|
||||
|
||||
export function BookingModal({
|
||||
@ -13,79 +15,104 @@ export function BookingModal({
|
||||
setEndTimeIndex,
|
||||
className,
|
||||
onClose,
|
||||
onNavigateToDetails,
|
||||
isOpen = true
|
||||
}) {
|
||||
const booking = useBookingContext();
|
||||
const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext();
|
||||
|
||||
// Initialize with pre-selected booking length if available, or auto-select if only 30 min available
|
||||
const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength :
|
||||
(hoursAvailable === 1 ? 1 : null); // Auto-select 30 min if that's all that's available
|
||||
const [selectedLength, setSelectedLength] = useState(null);
|
||||
const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex);
|
||||
// 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 modal opens
|
||||
useEffect(() => {
|
||||
if (initialLength && !hasInitialized.current) {
|
||||
setSelectedLength(initialLength);
|
||||
const newEndTime = startTimeIndex + initialLength;
|
||||
setCalculatedEndTime(newEndTime);
|
||||
setEndTimeIndex(newEndTime);
|
||||
booking.setSelectedEndIndex(newEndTime);
|
||||
if (initialEndTimeIndex && !hasInitialized.current) {
|
||||
setSelectedEndTimeIndex(initialEndTimeIndex);
|
||||
setEndTimeIndex(initialEndTimeIndex);
|
||||
booking.setSelectedEndIndex(initialEndTimeIndex);
|
||||
hasInitialized.current = true;
|
||||
}
|
||||
}, [initialLength, startTimeIndex, setEndTimeIndex, booking]);
|
||||
}, [initialEndTimeIndex, setEndTimeIndex, booking]);
|
||||
|
||||
const bookingLengths = [
|
||||
{ value: 1, label: "30 min" },
|
||||
{ value: 2, label: "1 h" },
|
||||
{ value: 3, label: "1.5 h" },
|
||||
{ value: 4, label: "2 h" },
|
||||
{ value: 5, label: "2.5 h" },
|
||||
{ value: 6, label: "3 h" },
|
||||
{ value: 7, label: "3.5 h" },
|
||||
{ value: 8, label: "4 h" },
|
||||
];
|
||||
|
||||
function getLabelFromAvailableHours(availableHours) {
|
||||
return bookingLengths.find(option => option.value === availableHours)?.label || "Välj längd";
|
||||
// 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);
|
||||
console.log('hoursAvailable:', hoursAvailable, 'originalHoursAvailable:', originalHoursAvailable.current, 'maxOptions:', maxOptions);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const disabledOptions = {
|
||||
1: !(hoursAvailable > 0),
|
||||
2: !(hoursAvailable > 1),
|
||||
3: !(hoursAvailable > 2),
|
||||
4: !(hoursAvailable > 3),
|
||||
5: !(hoursAvailable > 4),
|
||||
6: !(hoursAvailable > 5),
|
||||
7: !(hoursAvailable > 6),
|
||||
8: !(hoursAvailable > 7),
|
||||
};
|
||||
|
||||
function handleChange(event) {
|
||||
const lengthValue = event.target.value === "" ? null : parseInt(event.target.value);
|
||||
console.log(event.target.value);
|
||||
setSelectedLength(lengthValue);
|
||||
const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value);
|
||||
console.log('Selected end time value:', endTimeValue, 'Previous:', selectedEndTimeIndex);
|
||||
setSelectedEndTimeIndex(endTimeValue);
|
||||
|
||||
if (lengthValue !== null) {
|
||||
const newEndTime = startTimeIndex + lengthValue;
|
||||
setCalculatedEndTime(newEndTime);
|
||||
setEndTimeIndex(newEndTime);
|
||||
booking.setSelectedEndIndex(newEndTime);
|
||||
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
|
||||
setCalculatedEndTime(startTimeIndex);
|
||||
setEndTimeIndex(startTimeIndex);
|
||||
booking.setSelectedEndIndex(null);
|
||||
booking.setSelectedBookingLength && booking.setSelectedBookingLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has selected a booking length (including pre-selected)
|
||||
const hasSelectedLength = selectedLength !== null;
|
||||
// Check if user has selected an end time (including pre-selected)
|
||||
const hasSelectedEndTime = selectedEndTimeIndex !== null;
|
||||
|
||||
// Display time range - show calculated end time if length is selected
|
||||
const displayEndTime = hasSelectedLength ? calculatedEndTime : startTimeIndex;
|
||||
// Calculate duration in hours for display
|
||||
const calculateDuration = (endIndex) => {
|
||||
const durationSlots = endIndex - startTimeIndex;
|
||||
return durationSlots * 0.5; // Each slot is 30 minutes
|
||||
};
|
||||
|
||||
const handleNavigateToDetails = () => {
|
||||
console.log('handleNavigateToDetails called', { hasSelectedEndTime, onNavigateToDetails });
|
||||
|
||||
onNavigateToDetails();
|
||||
/*
|
||||
if (hasSelectedEndTime) {
|
||||
// Close modal first, then navigate
|
||||
onClose && onClose();
|
||||
setTimeout(() => {
|
||||
onNavigateToDetails && onNavigateToDetails();
|
||||
}, 100);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
@ -94,68 +121,49 @@ export function BookingModal({
|
||||
isDismissable
|
||||
onOpenChange={(open) => !open && onClose && onClose()}
|
||||
className={className}
|
||||
style={{borderRadius: '0.4rem', overflow: 'hidden'}}
|
||||
style={{borderRadius: '0.4rem', overflow: 'visible'}}
|
||||
>
|
||||
<Dialog style={{overflow: 'hidden'}}>
|
||||
<Dialog style={{overflow: 'visible'}}>
|
||||
<form>
|
||||
<Heading slot="title">{booking.title == "" ? getDefaultBookingTitle() : booking.title}</Heading>
|
||||
<p>{convertDateObjectToString(booking.selectedDate)}</p>
|
||||
<div className={styles.timeDisplay}>
|
||||
<div className={styles.timeRange}>
|
||||
<div className={styles.startTime}>
|
||||
<label>Starttid</label>
|
||||
<span className={styles.timeValue}>{getTimeFromIndex(startTimeIndex)}</span>
|
||||
</div>
|
||||
<div className={styles.timeSeparator}>–</div>
|
||||
<div className={styles.endTime}>
|
||||
<label>Sluttid</label>
|
||||
<span className={`${styles.timeValue} ${!hasSelectedLength ? styles.placeholder : ''}`}>
|
||||
{hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"}
|
||||
</span>
|
||||
<div className={styles.modalContent}>
|
||||
<Heading slot="title">Välj sluttid</Heading>
|
||||
<p>{convertDateObjectToString(booking.selectedDate)}</p>
|
||||
<div className={styles.timeDisplay}>
|
||||
<div className={styles.timeRange}>
|
||||
<div className={styles.startTimeSection}>
|
||||
<label>Starttid</label>
|
||||
<div className={styles.startTimeValue}>{getTimeFromIndex(startTimeIndex)}</div>
|
||||
</div>
|
||||
<div className={styles.endTimeSection}>
|
||||
<label>Sluttid</label>
|
||||
<Dropdown
|
||||
options={endTimeOptions}
|
||||
disabledOptions={disabledOptions}
|
||||
onChange={handleChange}
|
||||
value={selectedEndTimeIndex || ""}
|
||||
placeholder={!initialEndTimeIndex ? {
|
||||
value: "",
|
||||
label: "Välj sluttid"
|
||||
} : null}
|
||||
className={styles.endTimeDropdown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Längd</label>
|
||||
<Dropdown
|
||||
options={bookingLengths}
|
||||
disabledOptions={disabledOptions}
|
||||
onChange={handleChange}
|
||||
value={selectedLength || ""}
|
||||
placeholder={!initialLength ? {
|
||||
value: "",
|
||||
label: "Välj bokningslängd"
|
||||
} : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>{booking.selectedRoom !== "allRooms" ? "Rum" : "Tilldelat rum"}</label>
|
||||
<p>{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Deltagare</label>
|
||||
<p>
|
||||
{(() => {
|
||||
const currentUser = getCurrentUser();
|
||||
const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)];
|
||||
return allParticipants.map(p => p.name).join(", ");
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<Button className={styles.cancelButton} slot="close">
|
||||
Avbryt
|
||||
</Button>
|
||||
<Button
|
||||
className={`${styles.saveButton} ${!hasSelectedLength ? styles.disabledButton : ''}`}
|
||||
onClick={hasSelectedLength ? booking.handleSave : undefined}
|
||||
isDisabled={!hasSelectedLength}
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
|
||||
onClick={hasSelectedEndTime ? handleNavigateToDetails : undefined}
|
||||
disabled={!hasSelectedEndTime}
|
||||
>
|
||||
{hasSelectedLength ? 'Boka' : 'Välj längd först'}
|
||||
</Button>
|
||||
{hasSelectedEndTime ? 'Nästa' : 'Välj sluttid först'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
@ -139,54 +139,148 @@
|
||||
/* New time display styles */
|
||||
.timeDisplay {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background-color: var(--modal-display-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--modal-display-border);
|
||||
min-width: 196px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.timeRange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.startTime, .endTime {
|
||||
.startTimeSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.startTime label, .endTime label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
.endTimeSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.timeValue {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
.startTimeSection label, .endTimeSection label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.startTimeValue {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.timeValue.placeholder {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
.timeSeparator {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0.75rem;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Custom End Time Dropdown */
|
||||
.customEndTimeDropdown {
|
||||
position: relative;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.endTimeButton {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem 2.5rem 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.endTimeButton:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.endTimeButton:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-light);
|
||||
}
|
||||
|
||||
.endTimeButton::after {
|
||||
content: '▼';
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.8rem;
|
||||
color: var(--dropdown-chevron-color);
|
||||
}
|
||||
|
||||
.timeText {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.durationText {
|
||||
font-weight: 400;
|
||||
color: var(--text-tertiary);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.placeholderText {
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.timeSeparator {
|
||||
font-size: 1.5rem;
|
||||
.endTimeOptionsDropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.endTimeOption {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.endTimeOption:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.optionTime {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
font-feature-settings: 'tnum';
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.optionDuration {
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0.5rem;
|
||||
padding-top: 1.3rem;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Disabled button styles */
|
||||
@ -210,4 +304,51 @@
|
||||
.disabledButton:active {
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.bookingForms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Consistent modal sizing */
|
||||
:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal) {
|
||||
height: 550px !important;
|
||||
width: 400px !important;
|
||||
max-width: 90vw !important;
|
||||
min-height: 550px !important;
|
||||
max-height: 550px !important;
|
||||
}
|
||||
|
||||
:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal form) {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
.inlineForm {
|
||||
background: white;
|
||||
border: 1px solid #D1D5DB;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
background: var(--modal-bg);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--spacing-2xl);
|
||||
margin: var(--spacing-lg) 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideDown 0.2s ease-out;
|
||||
width: 100%;
|
||||
flex-basis: 100%;
|
||||
@ -22,7 +22,7 @@
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid #D1D5DB;
|
||||
border-bottom: 8px solid var(--border-light);
|
||||
}
|
||||
|
||||
.arrowLeft::after {
|
||||
@ -34,7 +34,7 @@
|
||||
height: 0;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid white;
|
||||
border-bottom: 7px solid var(--modal-bg);
|
||||
}
|
||||
|
||||
/* Arrow pointing to right card */
|
||||
@ -47,7 +47,7 @@
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid #D1D5DB;
|
||||
border-bottom: 8px solid var(--border-light);
|
||||
}
|
||||
|
||||
.arrowRight::after {
|
||||
@ -59,18 +59,18 @@
|
||||
height: 0;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid white;
|
||||
border-bottom: 7px solid var(--modal-bg);
|
||||
}
|
||||
|
||||
.formHeader {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.section:last-of-type {
|
||||
@ -78,86 +78,87 @@
|
||||
}
|
||||
|
||||
.formHeader h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dateText {
|
||||
margin: 0;
|
||||
color: #6B7280;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.timeDisplay {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.timeRange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #F9FAFB;
|
||||
border-radius: 0.375rem;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--modal-display-bg);
|
||||
border: 1px solid var(--modal-display-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.timeItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.timeItem label {
|
||||
font-size: 0.75rem;
|
||||
color: #6B7280;
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.timeValue {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.timeValue.placeholder {
|
||||
color: #9CA3AF;
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.timeSeparator {
|
||||
font-size: 1.5rem;
|
||||
color: #6B7280;
|
||||
font-weight: 300;
|
||||
font-size: var(--font-size-4xl);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.formField {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.formField label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.sectionWithTitle {
|
||||
padding-top: 1rem;
|
||||
padding-top: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.sectionWithTitle label {
|
||||
font-size: 0.8rem;
|
||||
color: #717171;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.sectionWithTitle p {
|
||||
@ -166,77 +167,77 @@
|
||||
|
||||
.formActions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
gap: var(--spacing-lg);
|
||||
margin-top: var(--spacing-3xl);
|
||||
padding-top: var(--spacing-2xl);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
flex: 1;
|
||||
background-color: white;
|
||||
background-color: var(--modal-cancel-bg);
|
||||
height: 2.75rem;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--modal-cancel-text);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
border: 2px solid var(--modal-cancel-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: var(--transition-medium);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
background-color: var(--modal-cancel-hover-bg);
|
||||
border-color: var(--modal-cancel-hover-border);
|
||||
}
|
||||
|
||||
.cancelButton:active {
|
||||
background-color: #e5e7eb;
|
||||
background-color: var(--modal-cancel-active-bg);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
flex: 2;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
background-color: var(--modal-save-bg);
|
||||
color: var(--modal-save-text);
|
||||
height: 2.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid #047857;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2);
|
||||
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);
|
||||
transition: var(--transition-medium);
|
||||
box-shadow: var(--modal-save-shadow);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background-color: #047857;
|
||||
box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3);
|
||||
background-color: var(--modal-save-hover-bg);
|
||||
box-shadow: var(--modal-save-hover-shadow);
|
||||
}
|
||||
|
||||
.saveButton:active {
|
||||
background-color: #065f46;
|
||||
background-color: var(--modal-save-active-bg);
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2);
|
||||
box-shadow: var(--modal-save-active-shadow);
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #adb5bd !important;
|
||||
border: 2px dashed #dee2e6 !important;
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
color: var(--button-disabled-text) !important;
|
||||
border: 2px dashed var(--button-disabled-border) !important;
|
||||
opacity: 0.6 !important;
|
||||
box-shadow: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.disabledButton:active {
|
||||
background-color: #f8f9fa !important;
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
|
||||
136
my-app/src/components/booking/InlineModalBookingForm.jsx
Normal file
136
my-app/src/components/booking/InlineModalBookingForm.jsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { convertDateObjectToString, getTimeFromIndex } from '../../helpers';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import { useBookingContext } from '../../context/BookingContext';
|
||||
import { useSettingsContext } from '../../context/SettingsContext';
|
||||
import styles from './InlineModalBookingForm.module.css';
|
||||
|
||||
export function InlineModalBookingForm({
|
||||
startTimeIndex,
|
||||
hoursAvailable,
|
||||
endTimeIndex,
|
||||
setEndTimeIndex,
|
||||
onClose,
|
||||
onNavigateToDetails,
|
||||
arrowPointsLeft = true
|
||||
}) {
|
||||
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 handleNavigateToDetails = () => {
|
||||
if (hasSelectedEndTime) {
|
||||
onNavigateToDetails && onNavigateToDetails();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
|
||||
{/* Header */}
|
||||
<div className={styles.formHeader}>
|
||||
{/*<h3 className={styles.formTitle}>Välj sluttid</h3>*/}
|
||||
{/* Time Selection */}
|
||||
<div className={styles.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>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className={styles.formActions}>
|
||||
<Button className={styles.cancelButton} onPress={onClose}>
|
||||
Avbryt
|
||||
</Button>
|
||||
<Button
|
||||
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
|
||||
onPress={hasSelectedEndTime ? handleNavigateToDetails : undefined}
|
||||
isDisabled={!hasSelectedEndTime}
|
||||
>
|
||||
{hasSelectedEndTime ? 'Nästa' : 'Välj sluttid först'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
192
my-app/src/components/booking/InlineModalBookingForm.module.css
Normal file
192
my-app/src/components/booking/InlineModalBookingForm.module.css
Normal file
@ -0,0 +1,192 @@
|
||||
.inlineForm {
|
||||
background: var(--modal-bg);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--spacing-2xl);
|
||||
margin: var(--spacing-lg) 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideDown 0.2s ease-out;
|
||||
width: 100%;
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Arrow pointing to left card */
|
||||
.arrowLeft::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 75px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid var(--border-light);
|
||||
}
|
||||
|
||||
.arrowLeft::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 76px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid var(--modal-bg);
|
||||
}
|
||||
|
||||
/* Arrow pointing to right card */
|
||||
.arrowRight::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 75px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid var(--border-light);
|
||||
}
|
||||
|
||||
.arrowRight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 76px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid var(--modal-bg);
|
||||
}
|
||||
|
||||
.formHeader {
|
||||
text-align: center;
|
||||
/*margin-bottom: var(--spacing-2xl);*/
|
||||
/*padding-bottom: var(--spacing-lg);*/
|
||||
/*border-bottom: 1px solid var(--border-light);*/
|
||||
}
|
||||
|
||||
.formTitle {
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dateText {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.formField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.formField label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.endTimeDropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.formActions {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
/*margin-top: var(--spacing-3xl);*/
|
||||
/*padding-top: var(--spacing-2xl);*/
|
||||
/*border-top: 1px solid var(--border-light);*/
|
||||
}
|
||||
|
||||
.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);
|
||||
transition: var(--transition-medium);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: var(--modal-cancel-hover-bg);
|
||||
border-color: var(--modal-cancel-hover-border);
|
||||
}
|
||||
|
||||
.cancelButton:active {
|
||||
background-color: var(--modal-cancel-active-bg);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
flex: 2;
|
||||
background-color: var(--modal-save-bg);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
.saveButton:active {
|
||||
background-color: var(--modal-save-active-bg);
|
||||
transform: translateY(1px);
|
||||
box-shadow: var(--modal-save-active-shadow);
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
color: var(--button-disabled-text) !important;
|
||||
border: 2px dashed var(--button-disabled-border) !important;
|
||||
opacity: 0.6 !important;
|
||||
box-shadow: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.disabledButton:active {
|
||||
background-color: var(--button-disabled-bg) !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,17 @@ export function ParticipantsSelector({ compact = false }) {
|
||||
itemRefs.current = [];
|
||||
};
|
||||
|
||||
const handleInputBlur = (e) => {
|
||||
// Small delay to allow click events on dropdown items to fire first
|
||||
setTimeout(() => {
|
||||
// Only close if the new focus target is not within our dropdown
|
||||
if (!dropdownRef.current?.contains(document.activeElement)) {
|
||||
setIsDropdownOpen(false);
|
||||
setFocusedIndex(-1);
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setIsDropdownOpen(true);
|
||||
@ -169,6 +180,7 @@ export function ParticipantsSelector({ compact = false }) {
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleInputFocus}
|
||||
onClick={handleInputClick}
|
||||
onBlur={handleInputBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search for participants..."
|
||||
className={compact ? styles.compactSearchInput : styles.searchInput}
|
||||
@ -196,6 +208,10 @@ export function ParticipantsSelector({ compact = false }) {
|
||||
ref={el => itemRefs.current[index] = el}
|
||||
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`}
|
||||
onClick={() => handleSelectPerson(person)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault(); // Prevent blur from firing
|
||||
handleSelectPerson(person);
|
||||
}}
|
||||
role="option"
|
||||
aria-selected={isPersonSelected(person.name)}
|
||||
>
|
||||
@ -232,6 +248,10 @@ export function ParticipantsSelector({ compact = false }) {
|
||||
ref={el => itemRefs.current[index] = el}
|
||||
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`}
|
||||
onClick={() => handleSelectPerson(person)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault(); // Prevent blur from firing
|
||||
handleSelectPerson(person);
|
||||
}}
|
||||
role="option"
|
||||
aria-selected={isPersonSelected(person.name)}
|
||||
>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.container {
|
||||
position: relative;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.elementHeading {
|
||||
@ -18,7 +19,6 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.participantChip {
|
||||
@ -310,4 +310,5 @@
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,18 +19,28 @@ const Navigation = () => {
|
||||
|
||||
// Prevent body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
// Store original overflow value
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
|
||||
if (menuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
document.body.style.overflow = originalOverflow || '';
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
// Cleanup on unmount - always restore scroll
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
document.body.style.overflow = originalOverflow || '';
|
||||
};
|
||||
}, [menuOpen]);
|
||||
|
||||
// Additional cleanup on component unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleCourses = () => {
|
||||
setCoursesOpen(!coursesOpen);
|
||||
};
|
||||
@ -60,7 +70,7 @@ const Navigation = () => {
|
||||
<div className={styles.top}>
|
||||
<div className={styles.left}>
|
||||
<Link to="/" className={styles.logo}>
|
||||
<img src="su-logo-darkblue.svg" alt="Logo" />
|
||||
<img src="su-logo-white.svg" alt="Logo" />
|
||||
</Link>
|
||||
<span className={styles.brandText}>Studentportalen</span>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-secondary);
|
||||
background-color: var(--su-blue);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-md);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -27,7 +29,6 @@
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--header-brand-color);
|
||||
}
|
||||
|
||||
.right {
|
||||
@ -39,18 +40,20 @@
|
||||
.logo img {
|
||||
height: 40px;
|
||||
transition: filter 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brandText {
|
||||
font-size: 1.1rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--header-brand-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
color: white;
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
@ -1,17 +1,55 @@
|
||||
import React from 'react';
|
||||
import styles from './Card.module.css'; // Import the CSS Module
|
||||
import styles from './Card.module.css';
|
||||
|
||||
const Card = ({ imageUrl, header, subheader, features = [], onClick, as: Component = 'div' }) => {
|
||||
const cardProps = {
|
||||
className: styles.card,
|
||||
onClick,
|
||||
...(Component === 'div' && {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
onKeyDown: (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClick?.(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const Card = ({ imageUrl, header, subheader }) => {
|
||||
return (
|
||||
<div className={styles.card} style={{ backgroundImage: `url(${imageUrl})` }}>
|
||||
<div className={styles.gradientOverlay}>
|
||||
<div className={styles.textContainer}>
|
||||
<h2 className={styles.header}>{header}</h2>
|
||||
<div className={styles.line}></div>
|
||||
<h3 className={styles.subheader}>{subheader}</h3>
|
||||
<Component {...cardProps}>
|
||||
<div className={styles.imageSection} style={{ backgroundImage: `url(${imageUrl})` }}>
|
||||
<div className={styles.imageOverlay}></div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contentSection}>
|
||||
<h3 className={styles.header}>{header}</h3>
|
||||
{subheader && (
|
||||
<p className={styles.subheader}>{subheader}</p>
|
||||
)}
|
||||
{features.length > 0 && (
|
||||
<div className={styles.features}>
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className={styles.feature}>
|
||||
{feature.icon && (
|
||||
<div className={styles.featureIcon}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
)}
|
||||
<span>{feature.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.actionSection}>
|
||||
<div className={styles.actionIcon}>
|
||||
→
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,46 +1,116 @@
|
||||
.card {
|
||||
width: 100%; /* Adjust width as needed */
|
||||
height: 300px; /* Adjust height as needed */
|
||||
display: flex;
|
||||
background: var(--bg-primary);
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: var(--border-radius-xl);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-medium);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.imageSection {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 100px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: var(--bg-secondary);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gradientOverlay {
|
||||
.imageOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, transparent, #05305E);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.textContainer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
.contentSection {
|
||||
flex: 1;
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header {
|
||||
margin: 0;
|
||||
font-size: 2em; /* Adjust font size as needed */
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 3px; /* Adjust thickness as needed */
|
||||
background-color: white;
|
||||
margin: 10px auto;
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.subheader {
|
||||
margin: 0;
|
||||
font-size: 1.2em; /* Adjust font size as needed */
|
||||
font-weight: normal;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--text-secondary);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-tertiary);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.actionSection {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-hover));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.actionSection::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.card:hover .actionSection::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ 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 }) => {
|
||||
const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "Select an option"}, disabledOptions = false, className }) => {
|
||||
return (
|
||||
<div className={styles.dropdownWrapper}>
|
||||
<div className={`${styles.dropdownWrapper} ${className || ''}`}>
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
.dropdownWrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.select {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TimeCard from './TimeCard';
|
||||
import { InlineBookingForm } from '../booking/InlineBookingForm';
|
||||
import { InlineModalBookingForm } from '../booking/InlineModalBookingForm';
|
||||
import { BookingModal } from '../booking/BookingModal';
|
||||
import styles from './TimeCardContainer.module.css';
|
||||
import modalStyles from '../booking/BookingModal.module.css';
|
||||
@ -10,11 +12,28 @@ import { useSettingsContext } from '../../context/SettingsContext';
|
||||
const SLOT_GROUPING_SIZE = 8;
|
||||
|
||||
export function TimeCardContainer() {
|
||||
const navigate = useNavigate();
|
||||
const booking = useBookingContext();
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
// Check if we should use inline form
|
||||
// Check booking form type
|
||||
const useInlineForm = settings.bookingFormType === 'inline';
|
||||
const useInlineModal = settings.bookingFormType === 'inline-modal';
|
||||
const useModal = settings.bookingFormType === 'modal';
|
||||
|
||||
const handleNavigateToDetails = () => {
|
||||
console.log('TimeCardContainer handleNavigateToDetails called, navigating to /booking-details');
|
||||
navigate('/booking-details', {
|
||||
state: {
|
||||
selectedDate: booking.selectedDate,
|
||||
selectedStartIndex: booking.selectedStartIndex,
|
||||
selectedEndIndex: booking.selectedEndIndex,
|
||||
assignedRoom: booking.assignedRoom,
|
||||
title: booking.title,
|
||||
participants: booking.participants
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const slotCount = 24; // 12 hours * 2 slots per hour (8:00 to 20:00)
|
||||
const slotIndices = Array.from({ length: slotCount }, (_, i) => i);
|
||||
@ -123,24 +142,39 @@ export function TimeCardContainer() {
|
||||
);
|
||||
|
||||
// Add inline booking form after the pair that contains the selected time card
|
||||
// Only show inline form if useInlineForm is true
|
||||
// Cards are laid out in pairs: (0,1), (2,3), (4,5), etc.
|
||||
if (useInlineForm && booking.selectedStartIndex !== null) {
|
||||
if ((useInlineForm || useInlineModal) && booking.selectedStartIndex !== null) {
|
||||
const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2;
|
||||
const selectedPairEnd = selectedPairStart + 1;
|
||||
|
||||
// Show form after the second card of the pair that contains the selected card
|
||||
if (slotIndex === selectedPairEnd && (booking.selectedStartIndex === selectedPairStart || booking.selectedStartIndex === selectedPairEnd)) {
|
||||
const isLeftCard = booking.selectedStartIndex === selectedPairStart;
|
||||
elements.push(
|
||||
<InlineBookingForm
|
||||
key={`form-${slotIndex}`}
|
||||
startTimeIndex={booking.selectedStartIndex}
|
||||
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
|
||||
onClose={() => booking.resetTimeSelections()}
|
||||
arrowPointsLeft={isLeftCard}
|
||||
/>
|
||||
);
|
||||
|
||||
if (useInlineForm) {
|
||||
elements.push(
|
||||
<InlineBookingForm
|
||||
key={`form-${slotIndex}`}
|
||||
startTimeIndex={booking.selectedStartIndex}
|
||||
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
|
||||
onClose={() => booking.resetTimeSelections()}
|
||||
arrowPointsLeft={isLeftCard}
|
||||
/>
|
||||
);
|
||||
} else if (useInlineModal) {
|
||||
elements.push(
|
||||
<InlineModalBookingForm
|
||||
key={`form-${slotIndex}`}
|
||||
startTimeIndex={booking.selectedStartIndex}
|
||||
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
|
||||
endTimeIndex={booking.selectedEndIndex}
|
||||
setEndTimeIndex={booking.setSelectedEndIndex}
|
||||
onClose={() => booking.resetTimeSelections()}
|
||||
onNavigateToDetails={handleNavigateToDetails}
|
||||
arrowPointsLeft={isLeftCard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,8 +185,8 @@ export function TimeCardContainer() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Show modal when a time slot is selected and not using inline form */}
|
||||
{!useInlineForm && booking.selectedStartIndex !== null && (
|
||||
{/* Show modal when a time slot is selected and using modal form type */}
|
||||
{useModal && booking.selectedStartIndex !== null && (
|
||||
<BookingModal
|
||||
startTimeIndex={booking.selectedStartIndex}
|
||||
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
|
||||
@ -160,6 +194,7 @@ export function TimeCardContainer() {
|
||||
setEndTimeIndex={booking.setSelectedEndIndex}
|
||||
className={modalStyles.modalContainer}
|
||||
onClose={() => booking.resetTimeSelections()}
|
||||
onNavigateToDetails={handleNavigateToDetails}
|
||||
isOpen={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -15,27 +15,62 @@ export function getTimeFromIndex(timeIndex) {
|
||||
}
|
||||
|
||||
export function convertDateObjectToString( date ) {
|
||||
const dayIndex = getDayOfWeek(date, "en-US");
|
||||
const monthIndex = date.month;
|
||||
|
||||
// Always use long format for now
|
||||
const isSmallScreen = false;
|
||||
|
||||
if (isSmallScreen) {
|
||||
const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"];
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"];
|
||||
try {
|
||||
const dayIndex = getDayOfWeek(date, "en-US");
|
||||
const monthIndex = date.month;
|
||||
|
||||
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
|
||||
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
|
||||
// Always use long format for now
|
||||
const isSmallScreen = false;
|
||||
|
||||
return `${dayOfWeek} ${date.day} ${monthName}`;
|
||||
} else {
|
||||
const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
|
||||
const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||||
|
||||
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
|
||||
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
|
||||
|
||||
return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
|
||||
if (isSmallScreen) {
|
||||
const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"];
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"];
|
||||
|
||||
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
|
||||
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
|
||||
|
||||
return `${dayOfWeek} ${date.day} ${monthName}`;
|
||||
} else {
|
||||
const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
|
||||
const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||||
|
||||
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
|
||||
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
|
||||
|
||||
return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error converting date to string:', error);
|
||||
// Fallback to a simple format if the date conversion fails
|
||||
if (date && typeof date === 'object' && date.day && date.month && date.year) {
|
||||
return `${date.day}/${date.month}/${date.year}`;
|
||||
}
|
||||
return 'Ogiltigt datum';
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBookingDate(date) {
|
||||
console.log('formatBookingDate called with:', date);
|
||||
|
||||
if (!date || !date.day || !date.month || !date.year) {
|
||||
console.log('Date validation failed:', { date, day: date?.day, month: date?.month, year: date?.year });
|
||||
return 'Ogiltigt datum';
|
||||
}
|
||||
|
||||
const months = ["januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", "oktober", "november", "december"];
|
||||
|
||||
const monthIndex = date.month;
|
||||
console.log('Month index:', monthIndex);
|
||||
const monthName = months[monthIndex - 1]; // month is 1-based, array is 0-based
|
||||
console.log('Month name:', monthName);
|
||||
|
||||
if (!monthName) {
|
||||
console.log('Month name not found, falling back');
|
||||
// If month is out of range, fall back to simple format
|
||||
return `${date.day}/${date.month}/${date.year}`;
|
||||
}
|
||||
|
||||
const result = `${date.day} ${monthName} ${date.year}`;
|
||||
console.log('Final result:', result);
|
||||
return result;
|
||||
}
|
||||
@ -187,6 +187,10 @@ export function useBookingState(addBooking, initialDate = null) {
|
||||
// Setters
|
||||
setTitle,
|
||||
setSelectedEndIndex,
|
||||
setSelectedDate,
|
||||
setSelectedStartIndex,
|
||||
setAssignedRoom,
|
||||
setParticipants,
|
||||
|
||||
// Handlers
|
||||
handleTimeCardClick,
|
||||
@ -222,5 +226,9 @@ export function useBookingState(addBooking, initialDate = null) {
|
||||
handleParticipantChange,
|
||||
handleRemoveParticipant,
|
||||
resetTimeSelections,
|
||||
setSelectedDate,
|
||||
setSelectedStartIndex,
|
||||
setAssignedRoom,
|
||||
setParticipants,
|
||||
]);
|
||||
}
|
||||
@ -30,6 +30,8 @@ body {
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
|
||||
132
my-app/src/pages/BookingDetails.jsx
Normal file
132
my-app/src/pages/BookingDetails.jsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import styles from './BookingDetails.module.css';
|
||||
import { BookingTitleField } from '../components/forms/BookingTitleField';
|
||||
import { ParticipantsSelector } from '../components/forms/ParticipantsSelector';
|
||||
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 BookingDetails({ addBooking }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { getEffectiveToday } = useSettingsContext();
|
||||
const booking = useBookingState(addBooking, getEffectiveToday());
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
// 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 handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
booking.handleSave();
|
||||
};
|
||||
|
||||
// Check if save button should be enabled (at least one other participant selected)
|
||||
const isSaveButtonEnabled = booking.participants && booking.participants.length > 0;
|
||||
|
||||
const toggleAccordion = () => {
|
||||
setIsAccordionOpen(!isAccordionOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<BookingProvider value={booking}>
|
||||
<div className={styles.pageContainer}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.pageTitle}>Ny bokning</h2>
|
||||
<div className={styles.dateTimeSection}>
|
||||
<div className={styles.dateContainer}>
|
||||
<span className={styles.date}>{booking.selectedDate ? formatBookingDate(booking.selectedDate) : 'Välj datum'}</span>
|
||||
</div>
|
||||
<div className={styles.timeContainer}>
|
||||
<span className={styles.time}>{getTimeFromIndex(booking.selectedStartIndex)} - {getTimeFromIndex(booking.selectedEndIndex)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainSection}>
|
||||
<button
|
||||
className={styles.backButton}
|
||||
onClick={handleBack}
|
||||
>
|
||||
← Tillbaka
|
||||
</button>
|
||||
|
||||
<div className={styles.formContainer}>
|
||||
<BookingTitleField />
|
||||
<ParticipantsSelector />
|
||||
</div>
|
||||
|
||||
<div className={styles.roomVisualization}>
|
||||
<div className={styles.roomArea}></div>
|
||||
<div className={`${styles.roomInfo} ${isAccordionOpen ? styles.expanded : ''}`} onClick={toggleAccordion}>
|
||||
<div className={styles.roomHeader}>
|
||||
<span>Lokal: {booking.assignedRoom || 'G5:12'}</span>
|
||||
<span className={`${styles.dropdown} ${isAccordionOpen ? styles.rotated : ''}`}>
|
||||
{isAccordionOpen ? '▲' : '▼'}
|
||||
</span>
|
||||
</div>
|
||||
{isAccordionOpen && (
|
||||
<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 className={styles.footer}>
|
||||
{!isSaveButtonEnabled && (
|
||||
<div className={styles.participantRequirement}>
|
||||
Lägg till minst en deltagare för att boka
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className={`${styles.saveButton} ${!isSaveButtonEnabled ? styles.disabledButton : ''}`}
|
||||
onClick={handleSave}
|
||||
disabled={!isSaveButtonEnabled}
|
||||
>
|
||||
Boka
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BookingProvider>
|
||||
);
|
||||
}
|
||||
242
my-app/src/pages/BookingDetails.module.css
Normal file
242
my-app/src/pages/BookingDetails.module.css
Normal file
@ -0,0 +1,242 @@
|
||||
.pageContainer {
|
||||
margin: 0 auto;
|
||||
min-height: calc(100vh - 4rem); /* Adjust for header/footer height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background-color: var(--bg-primary);
|
||||
position: sticky;
|
||||
top: 4rem;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.mainSection {
|
||||
padding: 1rem;
|
||||
padding-bottom: 8rem; /* Space for sticky footer */
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s ease;
|
||||
width: fit-content;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bookingInfo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dateTimeSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dateContainer,
|
||||
.timeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.date, .time {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.roomVisualization {
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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: 1rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.roomDetailItem {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.roomDetailItem:first-child {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.roomDetailItem:first-child .roomDetailLabel {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roomDetailLabel {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.roomDetailValue {
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-light);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background-color: var(--su-blue);
|
||||
background-color: #2D59F3;
|
||||
color: var(--su-white);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 4px rgba(0, 47, 95, 0.2);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background-color: var(--su-blue-80);
|
||||
box-shadow: 0 4px 8px rgba(0, 47, 95, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.saveButton:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 47, 95, 0.2);
|
||||
}
|
||||
|
||||
.saveButton:focus {
|
||||
outline: 2px solid var(--su-sky);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
background-color: var(--bg-tertiary, #e5e5e5) !important;
|
||||
color: var(--text-secondary, #666) !important;
|
||||
cursor: not-allowed !important;
|
||||
box-shadow: none !important;
|
||||
opacity: 0.8;
|
||||
border: 1px solid var(--border-light, #ddd) !important;
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background-color: var(--bg-tertiary, #e5e5e5) !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.disabledButton:active {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.participantRequirement {
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSettingsContext } from '../context/SettingsContext';
|
||||
import styles from './BookingSettings.module.css';
|
||||
|
||||
@ -31,6 +31,11 @@ export function BookingSettings() {
|
||||
return `${hours}:${minutes === 0 ? '00' : '30'}`;
|
||||
};
|
||||
|
||||
// Ensure body scroll is enabled when component mounts
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = '';
|
||||
}, []);
|
||||
|
||||
const effectiveToday = getEffectiveToday();
|
||||
const isUsingMockDate = settings.mockToday !== null;
|
||||
|
||||
@ -133,7 +138,7 @@ export function BookingSettings() {
|
||||
<label htmlFor="bookingFormType">
|
||||
<strong>Booking Form Type</strong>
|
||||
<span className={styles.description}>
|
||||
Choose between modal popup or inline form for creating bookings
|
||||
Choose between different booking form styles
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
@ -142,11 +147,21 @@ export function BookingSettings() {
|
||||
onChange={(e) => updateSettings({ bookingFormType: e.target.value })}
|
||||
className={styles.select}
|
||||
>
|
||||
<option value="inline">Inline Form (New)</option>
|
||||
<option value="inline">Inline Form (Complete)</option>
|
||||
<option value="modal">Modal Popup (Classic)</option>
|
||||
<option value="inline-modal">Inline Modal (Hybrid)</option>
|
||||
</select>
|
||||
<div className={styles.currentStatus}>
|
||||
Current: <strong>{settings.bookingFormType === 'inline' ? 'Inline Form' : 'Modal Popup'}</strong>
|
||||
Current: <strong>
|
||||
{settings.bookingFormType === 'inline' ? 'Inline Form' :
|
||||
settings.bookingFormType === 'modal' ? 'Modal Popup' :
|
||||
'Inline Modal'}
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import styles from './NewBooking.module.css';
|
||||
import { TimeCardContainer } from '../components/ui/TimeCardContainer';
|
||||
import { BookingDatePicker } from '../components/forms/BookingDatePicker';
|
||||
import { BookingTitleField } from '../components/forms/BookingTitleField';
|
||||
import { ParticipantsSelector } from '../components/forms/ParticipantsSelector';
|
||||
import { RoomSelectionField } from '../components/forms/RoomSelectionField';
|
||||
import { BookingLengthField } from '../components/forms/BookingLengthField';
|
||||
import { useBookingState } from '../hooks/useBookingState';
|
||||
@ -64,13 +62,6 @@ export function NewBooking({ addBooking }) {
|
||||
<h2>Boka litet grupprum</h2>
|
||||
<div className={styles.formContainer}>
|
||||
<main style={{ flex: 1 }}>
|
||||
{/* Only show title and participants fields in modal mode */}
|
||||
{!useInlineForm && (
|
||||
<>
|
||||
<BookingTitleField />
|
||||
<ParticipantsSelector />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.bookingTimesContainer}>
|
||||
<BookingDatePicker />
|
||||
|
||||
@ -10,6 +10,8 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure body scroll is enabled and scroll to top
|
||||
document.body.style.overflow = '';
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
@ -31,6 +33,10 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
|
||||
</div>
|
||||
)}
|
||||
<h1 className={styles.pageHeading}>Lokalbokning</h1>
|
||||
<h2 className={styles.sectionHeading}>Ny bokning</h2>
|
||||
<Link to='/new-booking'>
|
||||
<Card imageUrl="./grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />
|
||||
</Link>
|
||||
<h2 className={styles.sectionHeading}>Mina bokingar</h2>
|
||||
<BookingsList
|
||||
bookings={bookings}
|
||||
@ -48,10 +54,6 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
|
||||
showBookingDeleteBanner={settings.showBookingDeleteBanner}
|
||||
/>
|
||||
<hr className={styles.sectionDivider} />
|
||||
<h2 className={styles.sectionHeading}>Ny bokning</h2>
|
||||
<Link to='/new-booking'>
|
||||
<Card imageUrl="./grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
.react-aria-Calendar {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
color: var(--text-color);
|
||||
color: var(--text-primary);
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
@ -44,46 +44,46 @@
|
||||
}
|
||||
|
||||
&:hover:not([data-selected]):not([data-disabled]):not([data-unavailable]) {
|
||||
background-color: var(--highlight-hover);
|
||||
background-color: var(--bg-muted);
|
||||
}
|
||||
|
||||
&[data-pressed] {
|
||||
background: var(--gray-100);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--focus-ring-color);
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
background: var(--highlight-background);
|
||||
color: var(--highlight-foreground);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.react-aria-CalendarCell {
|
||||
&[data-disabled] {
|
||||
color: var(--text-color-disabled);
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.react-aria-CalendarCell {
|
||||
&[data-unavailable] {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-color-disabled);
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.react-aria-CalendarCell {
|
||||
&[data-invalid] {
|
||||
background: var(--invalid-color);
|
||||
color: var(--highlight-foreground);
|
||||
background: var(--notification-error-bg);
|
||||
color: var(--notification-error-title);
|
||||
}
|
||||
}
|
||||
|
||||
[slot=errorMessage] {
|
||||
font-size: 12px;
|
||||
color: var(--invalid-color);
|
||||
color: var(--notification-error-title);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
@import "./theme.css";
|
||||
|
||||
.react-aria-DatePicker {
|
||||
color: var(--text-color);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--border-light);
|
||||
@ -39,38 +39,34 @@
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, opacity 0.2s, color 0.2s;
|
||||
color: var(--chevron-button-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chevron-button:hover:not(:disabled) {
|
||||
background-color: var(--highlight-hover);
|
||||
background-color: var(--bg-muted);
|
||||
}
|
||||
|
||||
.chevron-button:active:not(:disabled) {
|
||||
background-color: var(--highlight-pressed);
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.chevron-button:disabled {
|
||||
cursor: default;
|
||||
color: var(--chevron-button-disabled-color);
|
||||
color: var(--text-disabled);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.chevron-button:focus-visible {
|
||||
outline: 2px solid var(--focus-ring-color);
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.react-aria-Button {
|
||||
/*background: var(--highlight-background);*/
|
||||
/*color: var(--highlight-foreground);*/
|
||||
border: 2px solid var(--field-background);
|
||||
background: var(--button-secondary-bg);
|
||||
color: var(--button-secondary-text);
|
||||
border: 1px solid var(--border-light);
|
||||
forced-color-adjust: none;
|
||||
border-radius: 4px;
|
||||
/*border: none;*/
|
||||
border: 1px solid var(--border-color);
|
||||
/*width: 1.429rem;*/
|
||||
/*height: 1.429rem;*/
|
||||
border-radius: var(--border-radius-sm);
|
||||
width: fit-content;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
@ -78,12 +74,11 @@
|
||||
|
||||
&[data-pressed] {
|
||||
box-shadow: none;
|
||||
/*background: var(--highlight-background);*/
|
||||
background: var(--button-background-pressed);
|
||||
background: var(--button-secondary-hover-bg);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--focus-ring-color);
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@ -95,12 +90,12 @@
|
||||
justify-content: space-between !important;
|
||||
gap: 0.75rem !important;
|
||||
cursor: pointer !important;
|
||||
background: var(--field-background) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
background: var(--input-bg) !important;
|
||||
border: 1px solid var(--input-border) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 12px 16px !important;
|
||||
font-weight: 500 !important;
|
||||
color: var(--field-text-color) !important;
|
||||
color: var(--input-text) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
||||
white-space: nowrap !important;
|
||||
@ -113,21 +108,21 @@
|
||||
}
|
||||
|
||||
.calendar-button:hover {
|
||||
border-color: var(--border-color-hover) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.calendar-button[data-pressed] {
|
||||
background: var(--button-background-pressed) !important;
|
||||
border-color: var(--border-color-pressed) !important;
|
||||
background: var(--button-secondary-hover-bg) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
transform: translateY(1px) !important;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.calendar-button[data-focus-visible] {
|
||||
outline: 2px solid var(--focus-ring-color) !important;
|
||||
outline: 2px solid var(--color-primary) !important;
|
||||
outline-offset: 2px !important;
|
||||
border-color: var(--focus-ring-color) !important;
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.calendar-date {
|
||||
@ -144,8 +139,6 @@
|
||||
|
||||
.react-aria-Popover[data-trigger=DatePicker] {
|
||||
max-width: unset;
|
||||
transform: translateX(-50%);
|
||||
left: 50% !important;
|
||||
}
|
||||
|
||||
.react-aria-DatePicker {
|
||||
@ -161,7 +154,7 @@
|
||||
|
||||
.react-aria-FieldError {
|
||||
font-size: 12px;
|
||||
color: var(--invalid-color);
|
||||
color: var(--notification-error-title);
|
||||
}
|
||||
|
||||
[slot=description] {
|
||||
|
||||
@ -90,7 +90,7 @@ export function DatePicker<T extends DateValue>(
|
||||
</Group>
|
||||
{description && <Text slot="description">{description}</Text>}
|
||||
<FieldError>{errorMessage}</FieldError>
|
||||
<Popover>
|
||||
<Popover placement="bottom" crossOffset={0}>
|
||||
<Dialog>
|
||||
<Calendar firstDayOfWeek={firstDayOfWeek}>
|
||||
<header>
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
.react-aria-Dialog {
|
||||
outline: none;
|
||||
padding: 30px;
|
||||
max-height: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
padding: 2rem;
|
||||
|
||||
.react-aria-Heading[slot=title] {
|
||||
line-height: 1em;
|
||||
|
||||
@ -4,21 +4,19 @@
|
||||
@import "./theme.css";
|
||||
|
||||
.react-aria-Popover {
|
||||
--background-color: var(--overlay-background);
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
|
||||
border-radius: 6px;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
max-width: 250px;
|
||||
transition: transform 200ms, opacity 200ms;
|
||||
|
||||
.react-aria-OverlayArrow svg {
|
||||
display: block;
|
||||
fill: var(--background-color);
|
||||
stroke: var(--border-color);
|
||||
fill: var(--bg-primary);
|
||||
stroke: var(--border-light);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
@ -354,6 +354,47 @@
|
||||
--loader-text-color: #333;
|
||||
--loader-border: rgba(0, 0, 0, 0.1);
|
||||
--loader-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* === STOCKHOLM UNIVERSITY BRAND COLORS === */
|
||||
|
||||
/* Primary university color */
|
||||
--su-blue: #002F5F;
|
||||
--su-blue-80: #33587F;
|
||||
|
||||
/* Secondary colors - Sky/Himmel */
|
||||
--su-sky: #ACDEE6;
|
||||
--su-sky-70: #C4E8ED;
|
||||
--su-sky-35: #E3F4F7;
|
||||
--su-sky-20: #EEF9FA;
|
||||
|
||||
/* Secondary colors - Water/Vatten */
|
||||
--su-water: #9BB2CE;
|
||||
--su-water-70: #B8C9DC;
|
||||
--su-water-35: #DCE4EE;
|
||||
--su-water-20: #EBF0F5;
|
||||
|
||||
/* Secondary colors - Fire/Eld */
|
||||
--su-fire: #EB7125;
|
||||
--su-fire-70: #F19B66;
|
||||
--su-fire-35: #F8CDB3;
|
||||
--su-fire-20: #FBE2D3;
|
||||
|
||||
/* Secondary colors - Olive/Oliv */
|
||||
--su-olive: #A3A86B;
|
||||
--su-olive-70: #BEC297;
|
||||
--su-olive-35: #DFE1CB;
|
||||
--su-olive-20: #EDEEE1;
|
||||
|
||||
/* Base colors */
|
||||
--su-dark-gray: #4B4B4B;
|
||||
--su-white: #FFFFFF;
|
||||
--su-medium-gray: #BABABA;
|
||||
--su-light-gray: #DADADA;
|
||||
|
||||
/* Utility colors - limited use */
|
||||
--su-green: #499943;
|
||||
--su-red: #B00020;
|
||||
--su-red-10: #F7E5E8;
|
||||
}
|
||||
|
||||
/* === DARK MODE === */
|
||||
@ -494,7 +535,7 @@
|
||||
--tooltip-text: #e5e7eb;
|
||||
|
||||
/* Button colors - dark mode */
|
||||
--button-bg: #374151;
|
||||
--button-bg: #28549c;
|
||||
--button-secondary-bg: #374151;
|
||||
--button-secondary-text: #e5e7eb;
|
||||
--button-secondary-hover-bg: #4b5563;
|
||||
@ -628,4 +669,46 @@
|
||||
--loader-text-color: #e5e7eb;
|
||||
--loader-border: rgba(255, 255, 255, 0.1);
|
||||
--loader-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
|
||||
|
||||
/* === STOCKHOLM UNIVERSITY - DARK MODE TEST === */
|
||||
|
||||
/* Primary university color */
|
||||
--su-blue: #132a42;
|
||||
--su-blue-80: #33587F;
|
||||
|
||||
/* Secondary colors - Sky/Himmel */
|
||||
--su-sky: #5d8388;
|
||||
--su-sky-70: #C4E8ED;
|
||||
--su-sky-35: #E3F4F7;
|
||||
--su-sky-20: #EEF9FA;
|
||||
|
||||
/* Secondary colors - Water/Vatten */
|
||||
--su-water: #9BB2CE;
|
||||
--su-water-70: #B8C9DC;
|
||||
--su-water-35: #DCE4EE;
|
||||
--su-water-20: #EBF0F5;
|
||||
|
||||
/* Secondary colors - Fire/Eld */
|
||||
--su-fire: #EB7125;
|
||||
--su-fire-70: #F19B66;
|
||||
--su-fire-35: #F8CDB3;
|
||||
--su-fire-20: #FBE2D3;
|
||||
|
||||
/* Secondary colors - Olive/Oliv */
|
||||
--su-olive: #A3A86B;
|
||||
--su-olive-70: #BEC297;
|
||||
--su-olive-35: #DFE1CB;
|
||||
--su-olive-20: #EDEEE1;
|
||||
|
||||
/* Base colors */
|
||||
--su-dark-gray: #4B4B4B;
|
||||
--su-white: #FFFFFF;
|
||||
--su-medium-gray: #BABABA;
|
||||
--su-light-gray: #DADADA;
|
||||
|
||||
/* Utility colors - limited use */
|
||||
--su-green: #499943;
|
||||
--su-red: #B00020;
|
||||
--su-red-10: #F7E5E8;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user