booking-flow-finalized-design kindaaaa #7

Merged
jare2473 merged 20 commits from booking-flow-finalized-design into main 2025-09-30 10:50:54 +02:00
10 changed files with 285 additions and 171 deletions
Showing only changes of commit 629700e6dc - Show all commits

View File

@@ -0,0 +1,12 @@
import React from 'react';
import styles from './PageContainer.module.css';
const PageContainer = ({ children, className = '' }) => {
return (
<div className={`${styles.pageContainer} ${className}`}>
{children}
</div>
);
};
export default PageContainer;

View File

@@ -0,0 +1,6 @@
.pageContainer {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-2xl);
min-height: 100vh;
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import styles from './PageHeader.module.css';
const PageHeader = ({ title, subtitle }) => {
return (
<div className={styles.header}>
<h1 className={styles.pageHeading}>{title}</h1>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</div>
);
};
export default PageHeader;

View File

@@ -0,0 +1,19 @@
.header {
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid var(--border-light);
}
.pageHeading {
color: var(--text-primary);
margin: 0 0 var(--spacing-md) 0;
font-size: 2.5rem;
font-weight: var(--font-weight-bold);
line-height: 1.2;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
font-weight: var(--font-weight-medium);
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import TimeCard from './TimeCard';
import { InlineBookingForm } from '../booking/InlineBookingForm';
@@ -11,12 +11,30 @@ import modalStyles from '../booking/BookingModal.module.css';
import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
const SLOT_GROUPING_SIZE = 8;
export function TimeCardContainer({ addBooking }) {
const navigate = useNavigate();
const booking = useBookingContext();
const { settings } = useSettingsContext();
const [slotGroupingSize, setSlotGroupingSize] = useState(8);
const LARGE_BREAKPOINT = 1400;
useEffect(() => {
const updateGroupingSize = () => {
const width = window.innerWidth;
if (width >= LARGE_BREAKPOINT + 1) {
setSlotGroupingSize(8); // 3 columns (24÷8=3)
} else if (width >= 769) {
setSlotGroupingSize(12); // 2 columns (24÷12=2)
} else {
setSlotGroupingSize(24); // 1 column (24÷24=1)
}
};
updateGroupingSize();
window.addEventListener('resize', updateGroupingSize);
return () => window.removeEventListener('resize', updateGroupingSize);
}, []);
// Check booking form type
const useInlineForm = settings.bookingFormType === 'inline';
@@ -59,161 +77,155 @@ export function TimeCardContainer({ addBooking }) {
function slotIndiciesToColumns(originalArray) {
let newArray = [];
let currentColumn = -1;
originalArray.map(index => {
if (index % SLOT_GROUPING_SIZE == 0) {
newArray.push(new Array(0));
currentColumn++;
const width = window.innerWidth;
if (width >= 769 && width <= LARGE_BREAKPOINT) {
// For medium screens: group in pairs first, then distribute pairs across 2 columns
const pairs = [];
for (let i = 0; i < originalArray.length; i += 2) {
pairs.push([originalArray[i], originalArray[i + 1]]);
}
newArray[currentColumn].push(originalArray[index]);
});
return newArray;
// Distribute pairs across 2 columns (6 pairs per column)
const column1 = pairs.slice(0, 6).flat();
const column2 = pairs.slice(6, 12).flat();
return [column1, column2];
} else {
// For other screen sizes: use original grouping logic
let newArray = [];
let currentColumn = -1;
originalArray.map(index => {
if (index % slotGroupingSize == 0) {
newArray.push(new Array(0));
currentColumn++;
}
newArray[currentColumn].push(originalArray[index]);
});
return newArray;
}
}
const renderColumn = (column, columnIndex) => {
const width = window.innerWidth;
if (width >= 769 && width <= LARGE_BREAKPOINT) {
// For medium screens: render pairs in rows
const pairs = [];
for (let i = 0; i < column.length; i += 2) {
pairs.push([column[i], column[i + 1]]);
}
return (
<div key={columnIndex} className={styles.column}>
{pairs.map((pair, pairIndex) => (
<div key={pairIndex} className={styles.pairRow}>
{pair.map(slotIndex => renderTimeCard(slotIndex))}
</div>
))}
</div>
);
} else if (width < 769) {
// For mobile: render pairs with spacing between every 4 pairs
const pairs = [];
for (let i = 0; i < column.length; i += 2) {
pairs.push([column[i], column[i + 1]]);
}
return (
<div key={columnIndex} className={styles.column}>
{pairs.map((pair, pairIndex) => (
<div key={pairIndex}>
<div className={styles.pairRow}>
{pair.map(slotIndex => renderTimeCard(slotIndex))}
</div>
{/* Add spacing after every 4th pair */}
{(pairIndex + 1) % 4 === 0 && pairIndex < pairs.length - 1 && (
<div className={styles.groupSpacer}></div>
)}
</div>
))}
</div>
);
} else {
// For large screens: render normally
return (
<div key={columnIndex} className={styles.column}>
{column.map(slotIndex => renderTimeCard(slotIndex))}
</div>
);
}
};
const renderTimeCard = (slotIndex) => {
let maxConsecutive = 0;
let roomId = "";
if (booking.currentRoom) {
const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex);
if (consecutive > maxConsecutive) {
maxConsecutive = consecutive;
}
} else {
booking.timeSlotsByRoom.forEach(room => {
const consecutive = countConsecutiveFromSlot(room.times, slotIndex);
if (consecutive > maxConsecutive) {
maxConsecutive = consecutive;
roomId = room.roomId;
}
});
}
if (booking.selectedBookingLength !== 0 && booking.selectedBookingLength <= maxConsecutive) {
maxConsecutive = booking.selectedBookingLength;
}
/* Set time card state here: */
let timeCardState = "unavailableSlot";
// If a booking length is pre-selected, only show slots that can accommodate the exact length
if (booking.selectedBookingLength !== 0) {
// Check if this slot can accommodate the selected booking length
const actualConsecutive = booking.currentRoom ?
countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) :
Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex)));
if (actualConsecutive >= booking.selectedBookingLength) {
timeCardState = "availableSlot";
}
} else {
// No pre-selected length, show if any time is available
if (maxConsecutive > 0) {
timeCardState = "availableSlot";
}
}
return (
<TimeCard
key={slotIndex}
startTimeIndex={slotIndex}
hoursAvailable={maxConsecutive}
handleClick={() => {
if (booking.selectedStartIndex === slotIndex) {
// If clicking on already selected card, close the form
booking.resetTimeSelections();
} else {
// Otherwise, select this card
booking.handleTimeCardClick(slotIndex, maxConsecutive, roomId);
}
}}
selected={slotIndex === booking.selectedStartIndex}
state={timeCardState}
handleTimeCardHover={() => {}}
handleTimeCardExit={booking.handleTimeCardExit}
/>
);
};
return (
<div>
<div className={styles.columnContainer}>
{slotIndiciesToColumns(slotIndices).map((column, index) => {
return (
<div key={index} className={styles.column}>
{column.map(slotIndex => {
let maxConsecutive = 0;
let roomId = "";
if (booking.currentRoom) {
const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex);
if (consecutive > maxConsecutive) {
maxConsecutive = consecutive;
}
} else {
booking.timeSlotsByRoom.forEach(room => {
const consecutive = countConsecutiveFromSlot(room.times, slotIndex);
if (consecutive > maxConsecutive) {
maxConsecutive = consecutive;
roomId = room.roomId;
}
});
}
if (booking.selectedBookingLength !== 0 && booking.selectedBookingLength <= maxConsecutive) {
maxConsecutive = booking.selectedBookingLength;
}
/* Set time card state here: */
let timeCardState = "unavailableSlot";
// If a booking length is pre-selected, only show slots that can accommodate the exact length
if (booking.selectedBookingLength !== 0) {
// Check if this slot can accommodate the selected booking length
const actualConsecutive = booking.currentRoom ?
countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) :
Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex)));
if (actualConsecutive >= booking.selectedBookingLength) {
timeCardState = "availableSlot";
}
} else {
// No pre-selected length, show if any time is available
if (maxConsecutive > 0) {
timeCardState = "availableSlot";
}
}
const elements = [];
elements.push(
<TimeCard
key={slotIndex}
startTimeIndex={slotIndex}
hoursAvailable={maxConsecutive}
handleClick={() => {
if (booking.selectedStartIndex === slotIndex) {
// If clicking on already selected card, close the form
booking.resetTimeSelections();
} else {
// Otherwise, select this card
booking.handleTimeCardClick(slotIndex, maxConsecutive, roomId);
}
}}
selected={slotIndex === booking.selectedStartIndex}
state={timeCardState}
handleTimeCardHover={() => {}}
handleTimeCardExit={booking.handleTimeCardExit}
/>
);
// 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 || useInlineModalExtendedNoLabels) && 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;
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}
/>
);
} else if (useInlineModalExtended) {
elements.push(
<InlineModalExtendedBookingForm
key={`form-${slotIndex}-${booking.selectedStartIndex}`}
startTimeIndex={booking.selectedStartIndex}
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
endTimeIndex={booking.selectedEndIndex}
setEndTimeIndex={booking.setSelectedEndIndex}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
addBooking={addBooking}
arrowPointsLeft={isLeftCard}
/>
);
} else if (useInlineModalExtendedNoLabels) {
elements.push(
<InlineModalExtendedBookingFormNoLabels
key={`form-${slotIndex}`}
startTimeIndex={booking.selectedStartIndex}
hoursAvailable={booking.selectedEndIndex - booking.selectedStartIndex}
endTimeIndex={booking.selectedEndIndex}
setEndTimeIndex={booking.setSelectedEndIndex}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
arrowPointsLeft={isLeftCard}
/>
);
}
}
}
return elements;
}).flat()}
</div>
)
})}
{slotIndiciesToColumns(slotIndices).map((column, index) => renderColumn(column, index))}
</div>
{/* Show modal when a time slot is selected and using modal form type */}

View File

@@ -1,7 +1,7 @@
.columnContainer {
display: flex;
flex-direction: row;
gap: 3rem;
gap: 2rem;
height: fit-content
}
@@ -35,10 +35,64 @@
display: none; /* Chrome, Safari, Opera */
}
@media (max-width: 1200px) {
.pairRow {
display: flex;
flex-direction: row;
gap: 0.5rem;
width: 100%;
justify-content: center;
}
/* Medium screens - 2 columns with 6 rows each */
@media (max-width: 1400px) and (min-width: 769px) {
.columnContainer {
display: flex;
flex-direction: row;
gap: 3rem;
justify-content: center;
}
.column {
min-width: 300px;
width: fit-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
height: fit-content;
align-items: flex-start;
justify-content: flex-start;
}
.pairRow {
display: flex;
flex-direction: row;
gap: 0.5rem;
width: 100%;
}
.pairRow > * {
flex: 0 0 135px;
width: 135px;
}
}
/* Small screens - single column */
@media (max-width: 768px) {
.columnContainer {
display: flex;
flex-direction: column;
gap: 3rem;
}
.column {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
align-items: center;
}
.groupSpacer {
height: 1.5rem;
}
}

View File

@@ -7,6 +7,8 @@ import { BookingLengthField } from '../components/forms/BookingLengthField';
import { useBookingState } from '../hooks/useBookingState';
import { BookingProvider } from '../context/BookingContext';
import { useSettingsContext } from '../context/SettingsContext';
import PageHeader from '../components/layout/PageHeader';
import PageContainer from '../components/layout/PageContainer';
export function NewBooking({ addBooking }) {
const { getEffectiveToday, settings } = useSettingsContext();
@@ -58,8 +60,8 @@ export function NewBooking({ addBooking }) {
return (
<BookingProvider value={booking}>
<div className={styles.pageContainer}>
<h2>Boka litet grupprum</h2>
<PageContainer>
<PageHeader title="Litet grupprum" subtitle="Plats för 5 personer" />
<div className={styles.formContainer}>
<main style={{ flex: 1 }}>
@@ -123,7 +125,7 @@ export function NewBooking({ addBooking }) {
</div>
</main>
</div>
</div>
</PageContainer>
</BookingProvider>
);
}

View File

@@ -5,6 +5,7 @@ import BookingsList from '../components/booking/BookingsList';
import Card from '../components/ui/Card';
import { useSettingsContext } from '../context/SettingsContext';
import { USER } from '../constants/bookingConstants';
import PageHeader from '../components/layout/PageHeader';
export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) {
const { settings } = useSettingsContext();
@@ -32,10 +33,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
</div>
</div>
)}
<div className={styles.header}>
<h1 className={styles.pageHeading}>Lokalbokning</h1>
<div className={styles.subtitle}>Reservera lokaler för möten och studier</div>
</div>
<PageHeader title="Lokalbokning" subtitle="Reservera lokaler för möten och studier" />
<h2 className={styles.sectionHeading}>Ny bokning</h2>

View File

@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import styles from './RoomSchedules.module.css';
import PageHeader from '../components/layout/PageHeader';
import PageContainer from '../components/layout/PageContainer';
const RoomSchedules = () => {
const [selectedBooking, setSelectedBooking] = useState(null);
@@ -145,8 +147,11 @@ const RoomSchedules = () => {
};
return (
<div className={styles.RoomSchedulesContainer}>
<h1>Room Schedules</h1>
<PageContainer>
<PageHeader
title="Room Schedules"
subtitle="View current room bookings and availability"
/>
<div className={styles.ScheduleWrapper}>
<div className={styles.TimeColumn}>
@@ -229,7 +234,7 @@ const RoomSchedules = () => {
</div>
</div>
)}
</div>
</PageContainer>
);
};

View File

@@ -1,10 +1,3 @@
.RoomSchedulesContainer {
padding: 1rem;
}
.RoomSchedulesContainer h1 {
margin-bottom: 1rem;
}
.ScheduleWrapper {
display: flex;