improving-week-36 #1

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

View File

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

View File

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

View File

@@ -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 bokning</h3>
<h3 className={compact ? styles.compactElementHeading : styles.elementHeading}>Titel 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}
/>
</>
);

View File

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

View File

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

View File

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

View File

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

View File

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