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
13 changed files with 387 additions and 112 deletions
Showing only changes of commit 2c943cdb20 - Show all commits

View File

@@ -144,9 +144,12 @@ export function InlineModalExtendedBookingForm({
// Check if user has selected an end time (including pre-selected)
const hasSelectedEndTime = selectedEndTimeIndex !== null;
// Check if user has added at least one additional participant (beyond themselves)
const hasRequiredParticipants = booking.participants.length > 0;
const handleSave = () => {
if (hasSelectedEndTime && addBooking) {
if (hasSelectedEndTime && hasRequiredParticipants && addBooking) {
console.log('Booking context state:', {
title: booking.title,
participants: booking.participants,
@@ -306,6 +309,15 @@ export function InlineModalExtendedBookingForm({
{/* Participants Field - Compact */}
<div className={extendedStyles.section}>
<ParticipantsSelector compact={true} />
{/*!hasRequiredParticipants && (
<div style={{
fontSize: '0.8rem',
color: '#6c757d',
marginTop: '0.5rem'
}}>
💡 Lägg till minst en deltagare
</div>
)*/}
</div>
</div>
@@ -317,11 +329,13 @@ export function InlineModalExtendedBookingForm({
Avbryt
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onPress={hasSelectedEndTime ? handleSave : undefined}
isDisabled={!hasSelectedEndTime}
className={`${styles.saveButton} ${(!hasSelectedEndTime || !hasRequiredParticipants) ? styles.disabledButton : ''}`}
onPress={(hasSelectedEndTime && hasRequiredParticipants) ? handleSave : undefined}
isDisabled={!hasSelectedEndTime || !hasRequiredParticipants}
>
{hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}
{!hasSelectedEndTime ? 'Välj sluttid först' :
!hasRequiredParticipants ? 'Minst en till deltagare krävs' :
'Boka'}
</Button>
</div>
</div>

View File

@@ -1,15 +1,22 @@
import React from 'react';
import styles from './PageHeader.module.css';
const PageHeader = ({ title, subtitle, imageUrl }) => {
const PageHeader = ({ title, subtitle, imageUrl, breadcrumbs }) => {
const headerClass = `${styles.header} ${imageUrl ? styles.withImage : styles.withoutImage}`;
return (
<div className={styles.header}>
<div className={headerClass}>
{imageUrl && (
<div className={styles.imageContainer}>
<img className={styles.image} src={imageUrl} alt={title} />
</div>
)}
<div className={styles.textContent}>
{breadcrumbs && (
<div className={styles.breadcrumbsContainer}>
{breadcrumbs}
</div>
)}
<h1 className={styles.pageHeading}>{title}</h1>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</div>

View File

@@ -1,6 +1,6 @@
.header {
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
/*margin-bottom: var(--spacing-xl);*/
padding: var(--spacing-2xl);
border-bottom: 1px solid var(--border-light);
display: flex;
flex-direction: column;
@@ -20,13 +20,16 @@
.textContent {
flex: 1;
padding: 1rem;
}
.breadcrumbsContainer {
margin-bottom: var(--spacing-md);
}
.pageHeading {
color: var(--text-primary);
margin: 0 0 var(--spacing-md) 0;
font-size: 2.5rem;
margin: 0;
font-size: 2rem;
font-weight: var(--font-weight-bold);
line-height: 1.2;
}
@@ -35,4 +38,21 @@
color: var(--text-secondary);
font-size: 1.1rem;
font-weight: var(--font-weight-medium);
}
/* Variant with image */
.withImage {
/* Default styles for image variant */
padding: 0;
}
.withImage .textContent {
padding: var(--spacing-2xl);
padding-top: var(--spacing-md);
}
@media (min-width: 750px) {
.image {
aspect-ratio: 11/3;
}
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Link } from 'react-router-dom';
import styles from './Breadcrumbs.module.css';
const Breadcrumbs = ({ items }) => {
return (
<nav className={styles.breadcrumbs} aria-label="Breadcrumb">
<ol className={styles.breadcrumbList}>
{items.map((item, index) => (
<li key={index} className={styles.breadcrumbItem}>
{index < items.length - 1 ? (
<Link to={item.path} className={styles.breadcrumbLink}>
{item.label}
</Link>
) : (
<span className={styles.breadcrumbCurrent} aria-current="page">
{item.label}
</span>
)}
{index < items.length - 1 && (
<span className={styles.breadcrumbSeparator} aria-hidden="true">
</span>
)}
</li>
))}
</ol>
</nav>
);
};
export default Breadcrumbs;

View File

@@ -0,0 +1,40 @@
.breadcrumbs {
margin-bottom: 0;
}
.breadcrumbList {
display: flex;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
color: var(--text-secondary);
}
.breadcrumbItem {
display: flex;
align-items: center;
}
.breadcrumbLink {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
}
.breadcrumbLink:hover {
color: var(--text-primary);
text-decoration: underline;
}
.breadcrumbCurrent {
color: var(--text-primary);
font-weight: 500;
}
.breadcrumbSeparator {
margin: 0 0.5rem;
color: var(--text-tertiary);
font-size: 0.75rem;
}

View File

@@ -11,7 +11,7 @@ import modalStyles from '../booking/BookingModal.module.css';
import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
export function TimeCardContainer({ addBooking }) {
export function TimeCardContainer({ addBooking, forceOneColumn = false }) {
const navigate = useNavigate();
const booking = useBookingContext();
const { settings } = useSettingsContext();
@@ -79,6 +79,11 @@ export function TimeCardContainer({ addBooking }) {
function slotIndiciesToColumns(originalArray) {
const width = window.innerWidth;
// Force single column if forceOneColumn prop is true
if (forceOneColumn) {
return [originalArray];
}
if (width >= 769 && width <= LARGE_BREAKPOINT) {
// For medium screens: group in pairs first, then distribute pairs across 2 columns
const pairs = [];
@@ -109,7 +114,7 @@ export function TimeCardContainer({ addBooking }) {
const renderColumn = (column, columnIndex) => {
const width = window.innerWidth;
if (width >= 769 && width <= LARGE_BREAKPOINT) {
if (width >= 769 && width <= LARGE_BREAKPOINT && !forceOneColumn) {
// For medium screens: render pairs in rows
const pairs = [];
for (let i = 0; i < column.length; i += 2) {
@@ -144,8 +149,8 @@ export function TimeCardContainer({ addBooking }) {
}).flat()}
</div>
);
} else if (width < 769) {
// For mobile: render pairs with spacing between every 4 pairs
} else if (width < 769 || forceOneColumn) {
// For mobile or forced single column: 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]]);

View File

@@ -33,6 +33,7 @@ export const SettingsProvider = ({ children }) => {
showBookingDeleteBanner: false,
bookingFormType: 'inline', // 'modal' or 'inline'
showFiltersAlways: false, // Show filter dropdowns always or behind toggle button
newBookingLayoutVariant: false, // false = stacked, true = side-by-side
// Then override with saved values
...parsed,
// Convert date strings back to DateValue objects
@@ -70,6 +71,8 @@ export const SettingsProvider = ({ children }) => {
bookingFormType: 'inline', // 'modal' or 'inline'
// Filter display mode
showFiltersAlways: false, // Show filter dropdowns always or behind toggle button
// New booking page layout variant
newBookingLayoutVariant: false, // false = stacked, true = side-by-side
};
});

View File

@@ -196,6 +196,31 @@ export function BookingSettings() {
<strong>Show Filter Button:</strong> Filters are hidden behind a collapsible button
</div>
</div>
<div className={styles.setting}>
<label htmlFor="newBookingLayoutVariant">
<strong>New Booking Page Layout</strong>
<span className={styles.description}>
Choose between stacked layout or side-by-side layout on medium screens
</span>
</label>
<div className={styles.toggleGroup}>
<input
id="newBookingLayoutVariant"
type="checkbox"
checked={settings.newBookingLayoutVariant}
onChange={(e) => updateSettings({ newBookingLayoutVariant: e.target.checked })}
className={styles.toggle}
/>
<span className={styles.toggleStatus}>
{settings.newBookingLayoutVariant ? 'Side-by-Side' : 'Stacked'}
</span>
</div>
<div className={styles.description} style={{marginTop: '0.5rem', fontSize: '0.85rem'}}>
<strong>Stacked:</strong> Image and header at top, booking times below<br/>
<strong>Side-by-Side:</strong> Image/header on left, booking times on right (medium screens)
</div>
</div>
</div>
<div className={styles.section}>

View File

@@ -9,6 +9,7 @@ import { BookingProvider } from '../context/BookingContext';
import { useSettingsContext } from '../context/SettingsContext';
import PageHeader from '../components/layout/PageHeader';
import PageContainer from '../components/layout/PageContainer';
import Breadcrumbs from '../components/ui/Breadcrumbs';
export function NewBooking({ addBooking }) {
const { getEffectiveToday, settings } = useSettingsContext();
@@ -58,75 +59,167 @@ export function NewBooking({ addBooking }) {
setShowFilters(false);
};
const breadcrumbItems = [
{ label: 'Lokalbokning', path: '/' },
{ label: 'Ny bokning', path: '/new-booking' }
];
return (
<BookingProvider value={booking}>
<PageHeader title="Litet grupprum" subtitle="Plats för 5 personer" imageUrl="./grupprum.jpg" />
<PageContainer>
{/* <img src="./grupprum.jpg" alt="Litet grupprum" className={styles.roomImage} /> */}
<div className={styles.formContainer}>
<main style={{ flex: 1 }}>
<div className={styles.bookingTimesContainer}>
<BookingDatePicker />
{/* Filter Section */}
<div className={styles.headerAndFilter}>
<div className={styles.filtersSection}>
{settings.showFiltersAlways ? (
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
</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>
<div className={`${styles.newBookingPageContainer} ${settings.newBookingLayoutVariant ? styles.sideBySide : ''}`}>
{settings.newBookingLayoutVariant ? (
/* Side-by-side layout */
<>
<div className={styles.headerSection}>
<PageHeader
title="Litet grupprum"
subtitle="Plats för 5 personer"
imageUrl="./grupprum.jpg"
breadcrumbs={<Breadcrumbs items={breadcrumbItems} />}
/>
</div>
<div className={styles.contentSection}>
<PageContainer>
<div className={styles.formContainer}>
<main style={{ flex: 1 }}>
<div className={styles.bookingTimesContainer}>
<BookingDatePicker />
{/* 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>
{/* Filter Section */}
<div className={styles.headerAndFilter}>
<div className={styles.filtersSection}>
{settings.showFiltersAlways ? (
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
</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>
)}
</>
)}
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
</div>
<div>
<TimeCardContainer addBooking={addBooking} forceOneColumn={settings.newBookingLayoutVariant} />
</div>
</div>
</main>
</div>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
</div>
<div>
<TimeCardContainer addBooking={addBooking} />
</div>
</PageContainer>
</div>
</main>
</div>
</PageContainer>
</>
) : (
/* Stacked layout (original) */
<>
<PageHeader
title="Litet grupprum"
subtitle="Plats för 5 personer"
imageUrl="./grupprum.jpg"
breadcrumbs={<Breadcrumbs items={breadcrumbItems} />}
/>
<div className={styles.formContainer}>
<main style={{ flex: 1 }}>
<div className={styles.bookingTimesContainer}>
<BookingDatePicker />
{/* Filter Section */}
<div className={styles.headerAndFilter}>
<div className={styles.filtersSection}>
{settings.showFiltersAlways ? (
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<BookingLengthField />
</div>
</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>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
</div>
<div>
<TimeCardContainer addBooking={addBooking} forceOneColumn={false} />
</div>
</div>
</main>
</div>
</>
)}
</div>
</BookingProvider>
);
}

View File

@@ -1,5 +1,6 @@
.pageContainer {
padding: var(--container-padding);
/*padding: var(--container-padding);*/
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
@@ -39,7 +40,7 @@
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg-secondary);
background-color: var(--bg-primary);
padding-bottom: 2rem;
}
@@ -300,4 +301,37 @@
opacity: 1;
transform: translateY(0);
}
}
/* Side-by-side layout variant */
.newBookingPageContainer {
/*padding: var(--spacing-lg);*/
}
.newBookingPageContainer.sideBySide {
display: flex;
flex-direction: column;
}
@media (min-width: 769px) and (max-width: 1400px) {
.newBookingPageContainer.sideBySide {
display: flex;
flex-direction: row;
gap: 2rem;
align-items: flex-start;
}
.newBookingPageContainer.sideBySide .headerSection {
flex: 0 0 400px;
max-width: 400px;
}
.newBookingPageContainer.sideBySide .contentSection {
flex: 1;
min-width: 0;
}
.newBookingPageContainer.sideBySide .bookingTimesContainer {
margin-top: 0;
}
}

View File

@@ -6,6 +6,7 @@ import Card from '../components/ui/Card';
import { useSettingsContext } from '../context/SettingsContext';
import { USER } from '../constants/bookingConstants';
import PageHeader from '../components/layout/PageHeader';
import PageContainer from '../components/layout/PageContainer';
export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) {
const { settings } = useSettingsContext();
@@ -24,7 +25,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
}
return (
<div className={styles.pageContainer}>
<>
{isTestSessionActive && (
<div className={styles.welcomeSection}>
<div className={styles.welcomeContent}>
@@ -34,37 +35,38 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
</div>
)}
<PageHeader title="Lokalbokning" subtitle="Reservera lokaler för möten och studier" />
<h2 className={styles.sectionHeading}>Ny bokning</h2>
<PageContainer>
<h2 className={styles.sectionHeading}>Ny bokning</h2>
<div className={styles.roomCategoryCards}>
<Link to='/new-booking'>
<Card imageUrl="./grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />
</Link>
<Link to='/new-booking'>
<Card imageUrl="./stort-grupprum.jpg" header="Stort grupprum" subheader="Plats för 10 personer" />
</Link>
</div>
<hr className={styles.sectionDivider} />
<div className={styles.roomCategoryCards}>
<Link to='/new-booking'>
<Card imageUrl="./grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />
</Link>
<Link to='/new-booking'>
<Card imageUrl="./stort-grupprum.jpg" header="Stort grupprum" subheader="Plats för 10 personer" />
</Link>
</div>
<hr className={styles.sectionDivider} />
<section id="bookings">
<h2 className={styles.sectionHeading}>Mina bokingar</h2>
<BookingsList
bookings={bookings}
handleEditBooking={handleEditBooking}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
showSuccessBanner={showSuccessBanner}
lastCreatedBooking={lastCreatedBooking}
onDismissBanner={onDismissBanner}
showDeleteBanner={showDeleteBanner}
lastDeletedBooking={lastDeletedBooking}
onDismissDeleteBanner={onDismissDeleteBanner}
showDevelopmentBanner={settings.showDevelopmentBanner}
showBookingConfirmationBanner={settings.showBookingConfirmationBanner}
showBookingDeleteBanner={settings.showBookingDeleteBanner}
/>
</section>
</div>
<section id="bookings">
<h2 className={styles.sectionHeading}>Mina bokingar</h2>
<BookingsList
bookings={bookings}
handleEditBooking={handleEditBooking}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
showSuccessBanner={showSuccessBanner}
lastCreatedBooking={lastCreatedBooking}
onDismissBanner={onDismissBanner}
showDeleteBanner={showDeleteBanner}
lastDeletedBooking={lastDeletedBooking}
onDismissDeleteBanner={onDismissDeleteBanner}
showDevelopmentBanner={settings.showDevelopmentBanner}
showBookingConfirmationBanner={settings.showBookingConfirmationBanner}
showBookingDeleteBanner={settings.showBookingDeleteBanner}
/>
</section>
</PageContainer>
</>
);
}

View File

@@ -10,7 +10,7 @@
color: var(--text-primary);
background-color: var(--bg-secondary);
padding: var(--spacing-md);
border: 1px solid var(--border-light);
border-bottom: 1px solid var(--border-light);
position: sticky;
width: 100%;
top: 0;

View File

@@ -298,7 +298,7 @@
/* Button disabled states */
--button-disabled-bg: #f8f9fa;
--button-disabled-text: #adb5bd;
--button-disabled-text: #797e83;
--button-disabled-border: #dee2e6;
/* Additional color variants */