booking-flow-finalized-design kindaaaa #7
12
my-app/src/components/layout/PageContainer.jsx
Normal file
12
my-app/src/components/layout/PageContainer.jsx
Normal 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;
|
||||
6
my-app/src/components/layout/PageContainer.module.css
Normal file
6
my-app/src/components/layout/PageContainer.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.pageContainer {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-2xl);
|
||||
min-height: 100vh;
|
||||
}
|
||||
13
my-app/src/components/layout/PageHeader.jsx
Normal file
13
my-app/src/components/layout/PageHeader.jsx
Normal 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;
|
||||
19
my-app/src/components/layout/PageHeader.module.css
Normal file
19
my-app/src/components/layout/PageHeader.module.css
Normal 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);
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
.RoomSchedulesContainer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.RoomSchedulesContainer h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ScheduleWrapper {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user