improving-week-36 #1
@@ -1,8 +1,108 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import styles from './BookingCard.module.css';
|
||||
import { convertDateObjectToString } from '../helpers';
|
||||
import Dropdown from './Dropdown';
|
||||
import { BookingTitleField } from './BookingTitleField';
|
||||
import { ParticipantsSelector } from './ParticipantsSelector';
|
||||
import { BookingProvider } from '../context/BookingContext';
|
||||
import { PEOPLE } from '../constants/bookingConstants';
|
||||
|
||||
function BookingCard({ booking, onClick, isExpanded, onBookingUpdate }) {
|
||||
const [selectedLength, setSelectedLength] = useState(null);
|
||||
const [calculatedEndTime, setCalculatedEndTime] = useState(null);
|
||||
const [editedTitle, setEditedTitle] = useState('');
|
||||
const [editedParticipants, setEditedParticipants] = useState([]);
|
||||
|
||||
// Calculate current booking length and available hours
|
||||
const currentLength = booking.endTime - booking.startTime;
|
||||
const maxAvailableTime = 16; // Max booking slots
|
||||
const hoursAvailable = Math.min(maxAvailableTime - booking.startTime, 8);
|
||||
|
||||
// Initialize state when card expands
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
setSelectedLength(currentLength);
|
||||
setCalculatedEndTime(booking.endTime);
|
||||
setEditedTitle(booking.title);
|
||||
setEditedParticipants(booking.participants || []);
|
||||
}
|
||||
}, [isExpanded, booking, currentLength]);
|
||||
|
||||
// Create a local booking context for the components
|
||||
const localBookingContext = {
|
||||
title: editedTitle,
|
||||
setTitle: setEditedTitle,
|
||||
participants: editedParticipants,
|
||||
handleParticipantChange: (participantId) => {
|
||||
const participant = PEOPLE.find(p => p.id === participantId);
|
||||
if (participant && !editedParticipants.find(p => p.id === participantId)) {
|
||||
setEditedParticipants(participants => [...participants, participant]);
|
||||
}
|
||||
},
|
||||
handleRemoveParticipant: (participantToRemove) => {
|
||||
setEditedParticipants(participants =>
|
||||
participants.filter(p => p.id !== participantToRemove.id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 && onBookingUpdate) {
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
title: editedTitle,
|
||||
participants: editedParticipants,
|
||||
endTime: calculatedEndTime
|
||||
};
|
||||
onBookingUpdate(updatedBooking);
|
||||
}
|
||||
onClick(); // Close the expanded view
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setSelectedLength(currentLength);
|
||||
setCalculatedEndTime(booking.endTime);
|
||||
setEditedTitle(booking.title);
|
||||
setEditedParticipants(booking.participants || []);
|
||||
onClick(); // Close the expanded view
|
||||
}
|
||||
|
||||
|
||||
function BookingCard({ booking, onClick }) {
|
||||
function getTimeFromIndex(timeIndex) {
|
||||
const totalHalfHoursFromStart = timeIndex;
|
||||
const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base
|
||||
@@ -32,8 +132,8 @@ function BookingCard({ booking, onClick }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.card} onClick={onClick}>
|
||||
<div className={styles.header}>
|
||||
<div className={`${styles.card} ${isExpanded ? styles.expanded : ''}`}>
|
||||
<div className={styles.header} onClick={!isExpanded ? onClick : undefined}>
|
||||
<div className={styles.leftSection}>
|
||||
<span className={styles.date}>{convertDateObjectToString(booking.date)}</span>
|
||||
<div className={styles.titleRow}>
|
||||
@@ -46,9 +146,53 @@ function BookingCard({ booking, onClick }) {
|
||||
</div>
|
||||
<div className={styles.timeSection}>
|
||||
<div className={styles.startTime}>{getTimeFromIndex(booking.startTime)}</div>
|
||||
<div className={styles.endTime}>{getTimeFromIndex(booking.endTime)}</div>
|
||||
<div className={styles.endTime}>{getTimeFromIndex(calculatedEndTime || booking.endTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<BookingProvider value={localBookingContext}>
|
||||
<div className={styles.expandedContent}>
|
||||
<div className={styles.formSection}>
|
||||
<BookingTitleField compact={true} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formSection}>
|
||||
<ParticipantsSelector compact={true} />
|
||||
</div>
|
||||
|
||||
<div className={styles.editSection}>
|
||||
<label className={styles.label}>Ändra längd</label>
|
||||
<Dropdown
|
||||
options={bookingLengths}
|
||||
disabledOptions={disabledOptions}
|
||||
onChange={handleLengthChange}
|
||||
value={selectedLength || ""}
|
||||
placeholder={{
|
||||
value: "",
|
||||
label: "Välj bokningslängd"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonSection}>
|
||||
<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>
|
||||
</div>
|
||||
</BookingProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,4 +107,252 @@
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Expanded card styles */
|
||||
.expanded {
|
||||
border-color: #007AFF;
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.12);
|
||||
}
|
||||
|
||||
.expanded:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.expanded .header {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.expandedContent {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #E5E5E5;
|
||||
}
|
||||
|
||||
.formSection {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.editSection {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #717171;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
flex: 2;
|
||||
background-color: white;
|
||||
height: 3rem;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.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: 3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Compact form inputs */
|
||||
.compactInput {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
background-color: white;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.compactInput:focus {
|
||||
outline: none;
|
||||
border-color: #007AFF;
|
||||
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.compactInput::placeholder {
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
/* Participant search styles */
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.searchDropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #D2D9E0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
padding: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdownItem:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dropdownItem:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.personName {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.personUsername {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.participantsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.participantChip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.participantName {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #1976D2;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.removeButton:hover {
|
||||
background-color: rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.removeButton:active {
|
||||
background-color: rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
|
||||
.noParticipants {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -3,18 +3,18 @@ import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants';
|
||||
import { useBookingContext } from '../context/BookingContext';
|
||||
import styles from './BookingTitleField.module.css';
|
||||
|
||||
export function BookingTitleField() {
|
||||
export function BookingTitleField({ compact = false }) {
|
||||
const booking = useBookingContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className={styles.elementHeading}>Titel på bokning</h3>
|
||||
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel på bokning</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={booking.title}
|
||||
onChange={(event) => booking.setTitle(event.target.value)}
|
||||
placeholder={DEFAULT_BOOKING_TITLE}
|
||||
className={styles.textInput}
|
||||
className={compact ? styles.compactTextInput : styles.textInput}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,4 +24,34 @@
|
||||
line-height: normal;
|
||||
margin-bottom: 0.2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Compact styles */
|
||||
.compactElementHeading {
|
||||
font-size: 0.75rem;
|
||||
color: #717171;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
margin-top: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.compactTextInput {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
background-color: white;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compactTextInput:focus {
|
||||
outline: 2px solid #007AFF;
|
||||
outline-offset: 2px;
|
||||
border-color: #007AFF;
|
||||
}
|
||||
@@ -3,25 +3,17 @@ 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, onBookingUpdate, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [selectedBooking, setSelectedBooking] = useState(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [expandedBookingId, setExpandedBookingId] = useState(null);
|
||||
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);
|
||||
setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -58,7 +50,13 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, showSucces
|
||||
{bookings.length > 0 ? (
|
||||
<>
|
||||
{displayedBookings.map((booking, index) => (
|
||||
<BookingCard key={index} booking={booking} onClick={() => handleBookingClick(booking)} />
|
||||
<BookingCard
|
||||
key={index}
|
||||
booking={booking}
|
||||
onClick={() => handleBookingClick(booking)}
|
||||
isExpanded={expandedBookingId === booking.id}
|
||||
onBookingUpdate={onBookingUpdate}
|
||||
/>
|
||||
))}
|
||||
{hasMoreBookings && (
|
||||
<button
|
||||
@@ -83,12 +81,6 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, showSucces
|
||||
<p className={styles.message}>Du har inga bokningar just nu</p>
|
||||
)}
|
||||
</div>
|
||||
<BookingDetailsModal
|
||||
booking={selectedBooking}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
onSave={onBookingUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "
|
||||
className={styles.select}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value={placeholder.value} disabled={false} hidden={false}>
|
||||
<option value={placeholder.value} disabled={true} hidden={false}>
|
||||
{placeholder.label}
|
||||
</option>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PEOPLE, USER } from '../constants/bookingConstants';
|
||||
import { useBookingContext } from '../context/BookingContext';
|
||||
import styles from './ParticipantsSelector.module.css';
|
||||
|
||||
export function ParticipantsSelector() {
|
||||
export function ParticipantsSelector({ compact = false }) {
|
||||
const booking = useBookingContext();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
@@ -155,8 +155,8 @@ export function ParticipantsSelector() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h3 className={styles.elementHeading}>Deltagare</h3>
|
||||
<div className={compact ? styles.compactContainer : styles.container}>
|
||||
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Deltagare</h3>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className={styles.searchContainer} ref={dropdownRef}>
|
||||
@@ -169,7 +169,7 @@ export function ParticipantsSelector() {
|
||||
onClick={handleInputClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search for participants..."
|
||||
className={styles.searchInput}
|
||||
className={compact ? styles.compactSearchInput : styles.searchInput}
|
||||
role="combobox"
|
||||
aria-expanded={isDropdownOpen}
|
||||
aria-autocomplete="list"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.participantChip {
|
||||
@@ -273,4 +274,37 @@
|
||||
color: #5F6368;
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Compact styles */
|
||||
.compactContainer {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compactElementHeading {
|
||||
font-size: 0.75rem;
|
||||
color: #717171;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
margin-top: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.compactSearchInput {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
background-color: white;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.compactSearchInput:focus {
|
||||
outline: 2px solid #007AFF;
|
||||
outline-offset: 2px;
|
||||
border-color: #007AFF;
|
||||
}
|
||||
Reference in New Issue
Block a user