eriks-booking-variant #6

Merged
jare2473 merged 13 commits from eriks-booking-variant into main 2025-09-22 11:16:13 +02:00
12 changed files with 285 additions and 48 deletions
Showing only changes of commit 490f4bba33 - Show all commits

View File

@@ -64,10 +64,11 @@
}
.formHeader {
text-align: center;
text-align: start;
/*margin-bottom: var(--spacing-2xl);*/
/*padding-bottom: var(--spacing-lg);*/
/*border-bottom: 1px solid var(--border-light);*/
margin-bottom: 1rem;
}
.formTitle {
@@ -84,7 +85,7 @@
}
.section {
margin-bottom: var(--spacing-2xl);
margin-bottom: 1.4rem;
}
.formField {
@@ -94,9 +95,9 @@
}
.formField label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--text-primary);
font-size: 0.75rem;
font-weight: 500;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -119,8 +120,8 @@
height: 2.75rem;
color: var(--modal-cancel-text);
font-weight: var(--font-weight-semibold);
border: 2px solid var(--modal-cancel-border);
border-radius: var(--border-radius-md);
border: 1px solid var(--modal-cancel-border);
border-radius: var(--border-radius-sm);
transition: var(--transition-medium);
cursor: pointer;
font-size: var(--font-size-sm);
@@ -139,24 +140,30 @@
.saveButton {
flex: 2;
background-color: var(--modal-save-bg);
background-color: #245CF8;
background-color: #1745E8;
color: var(--modal-save-text);
height: 2.75rem;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
border: 2px solid var(--modal-save-border);
border-radius: var(--border-radius-md);
border: 2px solid #0A3CC5;
border-radius: var(--border-radius-sm);
transition: var(--transition-medium);
box-shadow: var(--modal-save-shadow);
cursor: pointer;
}
.saveButton:hover {
background-color: var(--modal-save-hover-bg);
box-shadow: var(--modal-save-hover-shadow);
background-color: #052FC8;
border: 2px solid #0B2FAF;
}
.saveButton:active {
background-color: var(--modal-save-active-bg);
background-color: #072698;
border: 2px solid #092072;
transform: translateY(1px);
box-shadow: var(--modal-save-active-shadow);
}

View File

@@ -116,6 +116,7 @@ export function InlineModalExtendedBookingForm({
{/* Time Selection */}
<div className={extendedStyles.section}>
<div className={styles.formField}>
<label className={styles.formLabel}>Sluttid</label>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
@@ -141,6 +142,8 @@ export function InlineModalExtendedBookingForm({
</div>
</div>
<hr className={extendedStyles.divider} />
{/* Actions */}
<div className={styles.formActions}>
<Button className={styles.cancelButton} onPress={onClose}>

View File

@@ -1,11 +1,6 @@
/* Import base styles from the regular inline modal form */
@import './InlineModalBookingForm.module.css';
/* Extended form specific styles to ensure height alignment */
.section {
margin-bottom: var(--spacing-lg); /* Reduce spacing between sections */
}
/* Ensure all form inputs have consistent height */
.compactInput {
height: 2.5rem; /* Match dropdown height */
@@ -28,4 +23,9 @@
.compactHeading {
margin-bottom: 0.4rem;
margin-top: 0;
}
.divider {
margin: 1.5rem 0;
border: 0.6px solid var(--border-light);
}

View File

@@ -0,0 +1,161 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button } from 'react-aria-components';
import { useNavigate } from 'react-router-dom';
import { convertDateObjectToString, getTimeFromIndex } from '../../helpers';
import Dropdown from '../ui/Dropdown';
import { BookingTitleField } from '../forms/BookingTitleField';
import { ParticipantsSelector } from '../forms/ParticipantsSelector';
import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import styles from './InlineModalBookingForm.module.css';
import extendedStyles from './InlineModalExtendedBookingForm.module.css';
export function InlineModalExtendedBookingFormNoLabels({
startTimeIndex,
hoursAvailable,
endTimeIndex,
setEndTimeIndex,
onClose,
onNavigateToDetails,
arrowPointsLeft = true
}) {
const navigate = useNavigate();
const booking = useBookingContext();
const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext();
// Initialize with pre-selected end time if available, or auto-select if only 30 min available
const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength :
(hoursAvailable === 1 ? startTimeIndex + 1 : null); // Auto-select 30 min if that's all that's available
const [selectedEndTimeIndex, setSelectedEndTimeIndex] = useState(null);
const hasInitialized = useRef(false);
// Store the original hours available to prevent it from changing when selections are made
const originalHoursAvailable = useRef(hoursAvailable);
if (originalHoursAvailable.current < hoursAvailable) {
originalHoursAvailable.current = hoursAvailable;
}
// Effect to handle initial setup only once when form opens
useEffect(() => {
if (initialEndTimeIndex && !hasInitialized.current) {
setSelectedEndTimeIndex(initialEndTimeIndex);
setEndTimeIndex(initialEndTimeIndex);
booking.setSelectedEndIndex(initialEndTimeIndex);
hasInitialized.current = true;
}
}, [initialEndTimeIndex, setEndTimeIndex, booking]);
// Generate end time options based on available hours
const endTimeOptions = [];
const disabledOptions = {};
// Always show all possible options up to the original available hours, not limited by current selection
const maxOptions = Math.min(originalHoursAvailable.current, 8);
for (let i = 1; i <= maxOptions; i++) {
const endTimeIndex = startTimeIndex + i;
const endTime = getTimeFromIndex(endTimeIndex);
const durationLabel = i === 1 ? "30 min" :
i === 2 ? "1 h" :
i === 3 ? "1.5 h" :
i === 4 ? "2 h" :
i === 5 ? "2.5 h" :
i === 6 ? "3 h" :
i === 7 ? "3.5 h" :
i === 8 ? "4 h" : `${i * 0.5} h`;
endTimeOptions.push({
value: endTimeIndex,
label: `${endTime} · ${durationLabel}`
});
disabledOptions[endTimeIndex] = false; // All available options are enabled
}
function handleChange(event) {
const endTimeValue = event.target.value === "" ? null : parseInt(event.target.value);
setSelectedEndTimeIndex(endTimeValue);
if (endTimeValue !== null) {
setEndTimeIndex(endTimeValue);
booking.setSelectedEndIndex(endTimeValue);
// Update the selected booking length in context so it doesn't interfere
const newLength = endTimeValue - startTimeIndex;
booking.setSelectedBookingLength && booking.setSelectedBookingLength(newLength);
} else {
// Reset to default state when placeholder is selected
setEndTimeIndex(startTimeIndex);
booking.setSelectedEndIndex(null);
booking.setSelectedBookingLength && booking.setSelectedBookingLength(0);
}
}
// Check if user has selected an end time (including pre-selected)
const hasSelectedEndTime = selectedEndTimeIndex !== null;
const handleNavigateToConfirmation = () => {
if (hasSelectedEndTime) {
// Navigate directly to booking confirmation instead of details
navigate('/booking-confirmation', {
state: {
selectedDate: booking.selectedDate,
selectedStartIndex: booking.selectedStartIndex,
selectedEndIndex: booking.selectedEndIndex,
assignedRoom: booking.assignedRoom,
title: booking.title,
participants: booking.participants
}
});
}
};
return (
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
{/* Header */}
<div className={styles.formHeader}>
{/* Time Selection - No Label */}
<div className={extendedStyles.section}>
<div className={styles.formField}>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
onChange={handleChange}
value={selectedEndTimeIndex || ""}
placeholder={!initialEndTimeIndex ? {
value: "",
label: "Välj sluttid"
} : null}
className={styles.endTimeDropdown}
/>
</div>
</div>
{/* Title Field - Compact, No Label */}
<div className={extendedStyles.section}>
<BookingTitleField compact={true} hideLabel={true} />
</div>
{/* Participants Field - Compact, No Label */}
<div className={extendedStyles.section}>
<ParticipantsSelector compact={true} hideLabel={true} />
</div>
</div>
<hr className={extendedStyles.divider} />
{/* Actions */}
<div className={styles.formActions}>
<Button className={styles.cancelButton} onPress={onClose}>
Avbryt
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onPress={hasSelectedEndTime ? handleNavigateToConfirmation : undefined}
isDisabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}
</Button>
</div>
</div>
);
}

View File

@@ -3,18 +3,20 @@ import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import styles from './BookingTitleField.module.css';
export function BookingTitleField({ compact = false }) {
export function BookingTitleField({ compact = false, hideLabel = false }) {
const booking = useBookingContext();
const { getDefaultBookingTitle } = useSettingsContext();
return (
<>
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel bokning</h3>
{!hideLabel && (
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel bokning</h3>
)}
<input
type="text"
value={booking.title}
onChange={(event) => booking.setTitle(event.target.value)}
placeholder={getDefaultBookingTitle()}
placeholder={hideLabel ? "Titel på bokning" : getDefaultBookingTitle()}
className={compact ? styles.compactTextInput : styles.textInput}
/>
</>

View File

@@ -4,7 +4,7 @@ import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import styles from './ParticipantsSelector.module.css';
export function ParticipantsSelector({ compact = false }) {
export function ParticipantsSelector({ compact = false, hideLabel = false }) {
const booking = useBookingContext();
const { getCurrentUser } = useSettingsContext();
const [searchTerm, setSearchTerm] = useState('');
@@ -169,7 +169,9 @@ export function ParticipantsSelector({ compact = false }) {
return (
<div className={compact ? styles.compactContainer : styles.container}>
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Deltagare</h3>
{!hideLabel && (
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Deltagare</h3>
)}
{/* Search Input */}
<div className={styles.searchContainer} ref={dropdownRef}>
@@ -182,7 +184,7 @@ export function ParticipantsSelector({ compact = false }) {
onClick={handleInputClick}
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
placeholder="Sök deltagare..."
placeholder={hideLabel ? "Deltagare..." : "Sök deltagare..."}
className={compact ? styles.compactSearchInput : styles.searchInput}
role="combobox"
aria-expanded={isDropdownOpen}

View File

@@ -305,6 +305,7 @@
font-family: inherit;
transition: border-color 0.2s ease;
box-sizing: border-box;
margin-bottom: 0.4rem;
}
.compactSearchInput:focus {
@@ -313,3 +314,4 @@
border-color: var(--color-primary);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,10 +37,9 @@
.headerAndFilter {
width: 100%;
display: flex;
flex-direction: row;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 11.5rem;
padding-top: 2rem
}