new-modal #4

Merged
jare2473 merged 18 commits from new-modal into main 2025-09-10 10:01:30 +02:00
6 changed files with 406 additions and 25 deletions
Showing only changes of commit b9bfe5ee4b - Show all commits

View File

@@ -0,0 +1,135 @@
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}>
<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,18 @@ 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);
}
}, 20);
};
const handleInputChange = (e) => {
setSearchTerm(e.target.value);
setIsDropdownOpen(true);
@@ -169,6 +181,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}

View File

@@ -2,6 +2,7 @@ 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';
@@ -15,8 +16,10 @@ export function TimeCardContainer() {
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');
@@ -139,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}
/>
);
}
}
}
@@ -167,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}

View File

@@ -20,6 +20,7 @@
.mainSection {
padding: 1rem;
padding-bottom: 6rem; /* Space for sticky footer */
}
.backButton {
@@ -167,34 +168,46 @@
}
.footer {
margin-top: auto;
padding-top: 2rem;
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(--color-primary-dark);
color: var(--text-primary);
background-color: var(--su-blue);
color: var(--su-white);
border: none;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
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(--color-primary-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(1px);
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 47, 95, 0.2);
}
.saveButton:focus {
outline: 2px solid var(--color-primary);
outline: 2px solid var(--su-sky);
outline-offset: 2px;
}

View File

@@ -138,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
@@ -147,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>