new-modal #4

Merged
jare2473 merged 18 commits from new-modal into main 2025-09-10 10:01:30 +02:00
47 changed files with 1556 additions and 352 deletions

4
my-app/.gitignore vendored
View File

@ -25,3 +25,7 @@ dist-ssr
*storybook.log
storybook-static
# Font files
public/caecilia/
public/the-sans/

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
.dropdownWrapper {
position: relative;
display: inline-block;
width: 100%;
max-width: 200px;
}
.select {

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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] {

View File

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

View File

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

View File

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

View File

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