improving-week-36 #1
@@ -78,6 +78,12 @@ const AppRoutes = () => {
|
||||
setShowSuccessBanner(true);
|
||||
}
|
||||
|
||||
function updateBooking(updatedBooking) {
|
||||
setBookings(bookings.map(booking =>
|
||||
booking.id === updatedBooking.id ? updatedBooking : booking
|
||||
));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const timer = setTimeout(() => setLoading(false), 800);
|
||||
@@ -92,7 +98,7 @@ const AppRoutes = () => {
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} />} />
|
||||
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} onBookingUpdate={updateBooking} />} />
|
||||
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
|
||||
<Route path="booking-settings" element={<BookingSettings />} />
|
||||
</Route>
|
||||
|
||||
181
my-app/src/components/BookingDetailsModal.jsx
Normal file
181
my-app/src/components/BookingDetailsModal.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Dialog, Heading, Modal } from 'react-aria-components';
|
||||
import { convertDateObjectToString, getTimeFromIndex } from '../helpers';
|
||||
import Dropdown from './Dropdown';
|
||||
import styles from './BookingDetailsModal.module.css';
|
||||
|
||||
function BookingDetailsModal({ booking, isOpen, onClose, onSave }) {
|
||||
const [selectedLength, setSelectedLength] = useState(null);
|
||||
const [calculatedEndTime, setCalculatedEndTime] = useState(null);
|
||||
|
||||
// Calculate current booking length and available hours
|
||||
const currentLength = booking ? booking.endTime - booking.startTime : 1;
|
||||
// For simplicity, assume max booking time is 8 hours (16 half-hour slots)
|
||||
const maxAvailableTime = 16; // This could be dynamic based on room availability
|
||||
const hoursAvailable = booking ? Math.min(maxAvailableTime - booking.startTime, 8) : 8;
|
||||
|
||||
// Initialize state when modal opens or booking changes
|
||||
useEffect(() => {
|
||||
if (isOpen && booking) {
|
||||
setSelectedLength(currentLength);
|
||||
setCalculatedEndTime(booking.endTime);
|
||||
}
|
||||
}, [isOpen, booking, currentLength]);
|
||||
|
||||
if (!booking) return null;
|
||||
|
||||
const bookingLengths = [
|
||||
{ value: 1, label: "30 min" },
|
||||
{ value: 2, label: "1 h" },
|
||||
{ value: 3, label: "1.5 h" },
|
||||
{ value: 4, label: "2 h" },
|
||||
{ value: 5, label: "2.5 h" },
|
||||
{ value: 6, label: "3 h" },
|
||||
{ value: 7, label: "3.5 h" },
|
||||
{ value: 8, label: "4 h" },
|
||||
];
|
||||
|
||||
const disabledOptions = {
|
||||
1: !(hoursAvailable > 0),
|
||||
2: !(hoursAvailable > 1),
|
||||
3: !(hoursAvailable > 2),
|
||||
4: !(hoursAvailable > 3),
|
||||
5: !(hoursAvailable > 4),
|
||||
6: !(hoursAvailable > 5),
|
||||
7: !(hoursAvailable > 6),
|
||||
8: !(hoursAvailable > 7),
|
||||
};
|
||||
|
||||
function handleLengthChange(event) {
|
||||
const lengthValue = event.target.value === "" ? null : parseInt(event.target.value);
|
||||
setSelectedLength(lengthValue);
|
||||
|
||||
if (lengthValue !== null) {
|
||||
const newEndTime = booking.startTime + lengthValue;
|
||||
setCalculatedEndTime(newEndTime);
|
||||
} else {
|
||||
setCalculatedEndTime(booking.endTime);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (selectedLength !== null && onSave) {
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
endTime: calculatedEndTime
|
||||
};
|
||||
onSave(updatedBooking);
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setSelectedLength(currentLength);
|
||||
setCalculatedEndTime(booking.endTime);
|
||||
onClose();
|
||||
}
|
||||
|
||||
function formatParticipants(participants) {
|
||||
if (!participants || participants.length === 0) return 'Inga deltagare';
|
||||
|
||||
if (participants.length === 1) {
|
||||
return participants[0].name;
|
||||
} else if (participants.length === 2) {
|
||||
return `${participants[0].name} and ${participants[1].name}`;
|
||||
} else {
|
||||
const remaining = participants.length - 2;
|
||||
return `${participants[0].name}, ${participants[1].name} and ${remaining} more`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onClose}
|
||||
isDismissable
|
||||
className={styles.modalContainer}
|
||||
>
|
||||
<Dialog className={styles.dialog}>
|
||||
<div className={styles.header}>
|
||||
<Heading slot="title" className={styles.title}>
|
||||
{booking.title}
|
||||
</Heading>
|
||||
<Button
|
||||
className={styles.closeButton}
|
||||
onPress={onClose}
|
||||
aria-label="Stäng"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Datum</label>
|
||||
<p>{convertDateObjectToString(booking.date)}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.timeDisplay}>
|
||||
<div className={styles.timeRange}>
|
||||
<div className={styles.startTime}>
|
||||
<label>Starttid</label>
|
||||
<span className={styles.timeValue}>
|
||||
{getTimeFromIndex(booking.startTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.timeSeparator}>–</div>
|
||||
<div className={styles.endTime}>
|
||||
<label>Sluttid</label>
|
||||
<span className={styles.timeValue}>
|
||||
{getTimeFromIndex(calculatedEndTime || booking.endTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Längd</label>
|
||||
<Dropdown
|
||||
options={bookingLengths}
|
||||
disabledOptions={disabledOptions}
|
||||
onChange={handleLengthChange}
|
||||
value={selectedLength || ""}
|
||||
placeholder={{
|
||||
value: "",
|
||||
label: "Välj bokningslängd"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Rum</label>
|
||||
<p className={styles.roomText}>{booking.room}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.sectionWithTitle}>
|
||||
<label>Deltagare</label>
|
||||
<p>{formatParticipants(booking.participants)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.modalFooter}>
|
||||
<Button
|
||||
className={styles.cancelButton}
|
||||
onPress={handleCancel}
|
||||
>
|
||||
Avbryt
|
||||
</Button>
|
||||
<Button
|
||||
className={`${styles.saveButton} ${selectedLength === null ? styles.disabledButton : ''}`}
|
||||
onPress={handleSave}
|
||||
isDisabled={selectedLength === null}
|
||||
>
|
||||
{selectedLength !== null ? 'Spara ändringar' : 'Välj längd först'}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookingDetailsModal;
|
||||
244
my-app/src/components/BookingDetailsModal.module.css
Normal file
244
my-app/src/components/BookingDetailsModal.module.css
Normal file
@@ -0,0 +1,244 @@
|
||||
.modalContainer {
|
||||
background-color: transparent;
|
||||
width: 85%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18) !important;
|
||||
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12),
|
||||
0 16px 32px rgba(0, 0, 0, 0.08),
|
||||
0 8px 16px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
|
||||
backdrop-filter: blur(20px) saturate(140%) !important;
|
||||
-webkit-backdrop-filter: blur(20px) saturate(140%) !important;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
padding: 0;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.closeButton:focus {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.sectionWithTitle {
|
||||
padding-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sectionWithTitle label {
|
||||
font-size: 0.8rem;
|
||||
color: #717171;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sectionWithTitle p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timeDisplay {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
width: fit-content;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.timeRange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.startTime, .endTime {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.startTime label, .endTime label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.timeValue {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.timeSeparator {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: #6c757d;
|
||||
margin: 0 0.5rem;
|
||||
padding-top: 1.3rem;
|
||||
}
|
||||
|
||||
.roomText {
|
||||
font-weight: 600 !important;
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
flex: 2;
|
||||
background-color: white;
|
||||
height: 4rem;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.cancelButton:active {
|
||||
background-color: #e5e7eb;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
flex: 3;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
height: 4rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
border: 2px solid #047857;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background-color: #047857;
|
||||
box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
.saveButton:active {
|
||||
background-color: #065f46;
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
|
||||
.saveButton[data-focused],
|
||||
.cancelButton[data-focused] {
|
||||
outline: 2px solid #2563EB;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #adb5bd !important;
|
||||
border: 2px dashed #dee2e6 !important;
|
||||
opacity: 0.6 !important;
|
||||
box-shadow: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.disabledButton:active {
|
||||
background-color: #f8f9fa !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Modal overlay styles */
|
||||
:global(.react-aria-ModalOverlay) {
|
||||
z-index: 1100 !important;
|
||||
overflow-y: auto !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 2rem 1rem !important;
|
||||
box-sizing: border-box !important;
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
backdrop-filter: blur(12px) saturate(150%) !important;
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%) !important;
|
||||
}
|
||||
|
||||
:global(.react-aria-ModalOverlay .react-aria-Modal) {
|
||||
max-height: calc(100vh - 4rem) !important;
|
||||
max-width: 90vw !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
@@ -3,14 +3,27 @@ import { CalendarDate } from '@internationalized/date';
|
||||
import styles from './BookingsList.module.css';
|
||||
import BookingCard from './BookingCard';
|
||||
import BookingConfirmationBanner from './BookingConfirmationBanner';
|
||||
import BookingDetailsModal from './BookingDetailsModal';
|
||||
|
||||
function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) {
|
||||
function BookingsList({ bookings, handleEditBooking, onBookingUpdate, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [selectedBooking, setSelectedBooking] = useState(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const INITIAL_DISPLAY_COUNT = 3;
|
||||
|
||||
const displayedBookings = showAll ? bookings : bookings.slice(0, INITIAL_DISPLAY_COUNT);
|
||||
const hasMoreBookings = bookings.length > INITIAL_DISPLAY_COUNT;
|
||||
|
||||
function handleBookingClick(booking) {
|
||||
setSelectedBooking(booking);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
setIsModalOpen(false);
|
||||
setSelectedBooking(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.bookingsListContainer}>
|
||||
{showSuccessBanner && (
|
||||
@@ -45,7 +58,7 @@ function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCrea
|
||||
{bookings.length > 0 ? (
|
||||
<>
|
||||
{displayedBookings.map((booking, index) => (
|
||||
<BookingCard key={index} booking={booking} onClick={() => handleEditBooking(booking)} />
|
||||
<BookingCard key={index} booking={booking} onClick={() => handleBookingClick(booking)} />
|
||||
))}
|
||||
{hasMoreBookings && (
|
||||
<button
|
||||
@@ -70,6 +83,12 @@ function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCrea
|
||||
<p className={styles.message}>Du har inga bokningar just nu</p>
|
||||
)}
|
||||
</div>
|
||||
<BookingDetailsModal
|
||||
booking={selectedBooking}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
onSave={onBookingUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
.dropdownWrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.select {
|
||||
font-family: inherit;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
padding: 0.5rem 2rem 0.5rem 1rem; /* Make room on right for chevron */
|
||||
padding: 0.5rem 2.5rem 0.5rem 1rem; /* More room on right for chevron */
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.375rem;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
width: fit-content;
|
||||
min-width: 6rem; /* Optional: prevent it from getting too small */
|
||||
width: 100%;
|
||||
min-width: 150px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.75rem;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import BookingsList from '../components/BookingsList';
|
||||
import Card from '../components/Card';
|
||||
import { useSettingsContext } from '../context/SettingsContext';
|
||||
|
||||
export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner }) {
|
||||
export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate }) {
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
function handleEditBooking(booking) {
|
||||
@@ -20,6 +20,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
|
||||
<BookingsList
|
||||
bookings={bookings}
|
||||
handleEditBooking={handleEditBooking}
|
||||
onBookingUpdate={onBookingUpdate}
|
||||
showSuccessBanner={showSuccessBanner}
|
||||
lastCreatedBooking={lastCreatedBooking}
|
||||
onDismissBanner={onDismissBanner}
|
||||
|
||||
Reference in New Issue
Block a user