improving-week-36 #1

Merged
jare2473 merged 41 commits from improving-week-36 into main 2025-09-04 10:49:05 +02:00
6 changed files with 463 additions and 8 deletions
Showing only changes of commit 8eed906189 - Show all commits

View File

@@ -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>

View 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;

View 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;
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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}