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
5 changed files with 337 additions and 119 deletions
Showing only changes of commit 06bb36e80e - Show all commits

View File

@@ -33,6 +33,7 @@ const AppRoutes = () => {
roomCategory: 'green',
title: 'Team standup',
participants: [
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
{ id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' },
{ id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' },
{ id: 4, name: 'Elin Rudling', username: 'elru4521', email: 'elin.rudling@dsv.su.se' }
@@ -47,6 +48,7 @@ const AppRoutes = () => {
roomCategory: 'red',
title: 'Project planning workshop',
participants: [
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
{ id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' },
{ id: 6, name: 'Ellen Britschgi', username: 'elbr5623', email: 'ellen.britschgi@dsv.su.se' },
{ id: 7, name: 'Anna Andersson', username: 'anan3457', email: 'anna.andersson@dsv.su.se' },
@@ -64,6 +66,7 @@ const AppRoutes = () => {
roomCategory: 'blue',
title: '1:1 with supervisor',
participants: [
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
{ id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }
]
},
@@ -76,9 +79,27 @@ const AppRoutes = () => {
roomCategory: 'yellow',
title: 'Study group session',
participants: [
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
{ id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' },
{ id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' }
]
},
{
id: 5,
date: new CalendarDate(2025, 9, 7),
startTime: 10,
endTime: 12,
room: 'G5:9',
roomCategory: 'blue',
title: 'Design review meeting',
createdBy: { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' },
participants: [
{ id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' },
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
{ id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' },
{ id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' }
],
isParticipantBooking: true
}
]);

View File

@@ -6,9 +6,11 @@ import Dropdown from '../ui/Dropdown';
import { BookingTitleField } from '../forms/BookingTitleField';
import { ParticipantsSelector } from '../forms/ParticipantsSelector';
import { BookingProvider } from '../../context/BookingContext';
import { PEOPLE } from '../../constants/bookingConstants';
import { PEOPLE, USER } from '../../constants/bookingConstants';
function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) {
// Check if this is a participant booking (user was added by someone else)
const isParticipantBooking = booking.isParticipantBooking === true;
const [selectedLength, setSelectedLength] = useState(null);
const [calculatedEndTime, setCalculatedEndTime] = useState(null);
const [editedTitle, setEditedTitle] = useState('');
@@ -118,6 +120,14 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD
setShowDeleteConfirm(false);
}
function handleRemoveSelf() {
// For participant bookings, remove the current user from participants
// This effectively "leaves" the booking
if (isParticipantBooking && onBookingDelete) {
onBookingDelete(booking);
}
}
function getTimeFromIndex(timeIndex) {
const totalHalfHoursFromStart = timeIndex;
@@ -133,13 +143,35 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD
function formatParticipants(participants) {
if (!participants || participants.length === 0) return null;
const formatName = (participant, index) => {
// Bold the booker's name (creator in participant bookings, or current user in regular bookings)
const isBooker = isParticipantBooking
? (booking.createdBy && participant.id === booking.createdBy.id)
: (participant.id === USER.id);
const firstName = participant.name.split(' ')[0];
if (isBooker) {
return <strong key={`participant-${participant.id}-${index}`}>{firstName}</strong>;
}
return <span key={`participant-${participant.id}-${index}`}>{firstName}</span>;
};
if (participants.length === 1) {
return participants[0].name;
return formatName(participants[0], 0);
} else if (participants.length === 2) {
return `${participants[0].name} and ${participants[1].name}`;
return (
<>
{formatName(participants[0], 0)} and {formatName(participants[1], 1)}
</>
);
} else {
const remaining = participants.length - 2;
return `${participants[0].name}, ${participants[1].name} and ${remaining} more`;
return (
<>
{formatName(participants[0], 0)}, {formatName(participants[1], 1)} and {remaining} more
</>
);
}
}
@@ -147,95 +179,166 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingD
<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}>
<h3 className={styles.title}>{booking.title}</h3>
</div>
<div className={styles.roomAndParticipants}>
<span className={styles.room}>{booking.room}</span>
{booking.participants && booking.participants.length > 0 && (
<p className={styles.participants}>{formatParticipants(booking.participants)}</p>
)}
</div>
{isParticipantBooking && booking.createdBy && (
<div className={styles.createdBy}>
Tillagd av {booking.createdBy.name}
</div>
)}
{!isParticipantBooking && booking.participants && booking.participants.length > 0 && (
<div className={styles.participants}>{formatParticipants(booking.participants)}</div>
)}
</div>
<div className={styles.timeSection}>
<div className={styles.startTime}>{getTimeFromIndex(booking.startTime)}</div>
<div className={styles.endTime}>{getTimeFromIndex(calculatedEndTime || booking.endTime)}</div>
<div className={styles.rightSection}>
<div className={styles.time}>
{getTimeFromIndex(booking.startTime)} {getTimeFromIndex(calculatedEndTime || booking.endTime)}
</div>
<div className={styles.roomBadge}>
{booking.room}
</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>
{!showDeleteConfirm ? (
<div className={styles.buttonSection}>
<Button
className={styles.deleteButton}
onPress={handleDelete}
>
Radera
</Button>
<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>
{isParticipantBooking ? (
// Participant booking view - read-only with remove self option
<>
<div className={styles.readOnlySection}>
<div className={styles.readOnlyField}>
<label className={styles.label}>Bokning skapad av</label>
<p className={styles.createdByText}>{booking.createdBy?.name}</p>
</div>
<div className={styles.readOnlyField}>
<label className={styles.label}>Deltagare</label>
<p className={styles.participantsText}>
{booking.participants
?.filter(p => p.id !== booking.createdBy?.id)
?.map(p => p.name)
?.join(', ')}
</p>
</div>
</div>
{!showDeleteConfirm ? (
<div className={styles.buttonSection}>
<Button
className={styles.deleteButton}
onPress={() => setShowDeleteConfirm(true)}
>
Lämna bokning
</Button>
<Button
className={styles.cancelButton}
onPress={handleCancel}
>
Stäng
</Button>
</div>
) : (
<div className={styles.confirmationSection}>
<div className={styles.confirmationMessage}>
<span className={styles.warningIcon}></span>
<p>Är du säker att du vill lämna denna bokning?</p>
<p className={styles.bookingDetails}>
Du kommer inte längre att vara med "{booking.title}" den {booking.date.day}/{booking.date.month}
</p>
</div>
<div className={styles.confirmationButtons}>
<Button
className={styles.confirmDeleteButton}
onPress={handleRemoveSelf}
>
Ja, lämna bokning
</Button>
<Button
className={styles.cancelDeleteButton}
onPress={cancelDelete}
>
Avbryt
</Button>
</div>
</div>
)}
</>
) : (
<div className={styles.confirmationSection}>
<div className={styles.confirmationMessage}>
<span className={styles.warningIcon}></span>
<p>Är du säker att du vill radera denna bokning?</p>
<p className={styles.bookingDetails}>
"{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)}
</p>
// Regular booking view - editable
<>
<div className={styles.formSection}>
<BookingTitleField compact={true} />
</div>
<div className={styles.confirmationButtons}>
<Button
className={styles.confirmDeleteButton}
onPress={confirmDelete}
>
Ja, radera
</Button>
<Button
className={styles.cancelDeleteButton}
onPress={cancelDelete}
>
Avbryt
</Button>
<div className={styles.formSection}>
<ParticipantsSelector compact={true} />
</div>
</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>
{!showDeleteConfirm ? (
<div className={styles.buttonSection}>
<Button
className={styles.deleteButton}
onPress={handleDelete}
>
Radera
</Button>
<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 className={styles.confirmationSection}>
<div className={styles.confirmationMessage}>
<span className={styles.warningIcon}></span>
<p>Är du säker att du vill radera denna bokning?</p>
<p className={styles.bookingDetails}>
"{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)}
</p>
</div>
<div className={styles.confirmationButtons}>
<Button
className={styles.confirmDeleteButton}
onPress={confirmDelete}
>
Ja, radera
</Button>
<Button
className={styles.cancelDeleteButton}
onPress={cancelDelete}
>
Avbryt
</Button>
</div>
</div>
)}
</>
)}
</div>
</BookingProvider>

View File

@@ -1,56 +1,83 @@
.card {
border: var(--border-width-thin) solid var(--border-light);
padding: var(--card-padding);
padding: 1rem;
width: 100%;
border-radius: var(--border-radius-md);
background: var(--bg-primary);
transition: var(--transition-medium);
box-shadow: var(--shadow-lg);
/*box-shadow: var(--shadow-lg);*/
}
.card:hover {
cursor: pointer;
border-color: var(--color-primary);
box-shadow: var(--shadow-xl);
/*transform: translateY(-2px);*/
@media (hover: hover) {
.card:hover {
cursor: pointer;
border-color: var(--color-primary);
box-shadow: var(--shadow-xl);
/*transform: translateY(-2px);*/
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
align-items: flex-start;
gap: 1rem;
width: 100%;
}
.leftSection {
flex: 1;
}
.date {
text-transform: uppercase;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--text-muted);
letter-spacing: 1px;
margin-bottom: var(--spacing-sm);
display: block;
}
.titleRow {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-sm);
flex-direction: column;
gap: 0.5rem;
height: 100%;
}
.rightSection {
display: flex;
flex-direction: column;
align-items: flex-end;
align-items: end
}
.createdByIndicator {
font-size: var(--font-size-sm);
color: var(--text-muted);
font-weight: var(--font-weight-normal);
font-style: italic;
}
.title {
margin: 0;
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
.time {
font-size: 1.1rem;
font-weight: 600;
color: #111827;
white-space: nowrap;
}
.createdBy {
font-size: 0.875rem;
color: #6b7280;
}
.roomBadge {
background-color: #065f46;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 10rem;
font-size: 1rem;
font-weight: 500;
white-space: nowrap;
}
.room {
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-xl);
@@ -89,8 +116,8 @@
.participants {
margin: 0;
font-size: var(--font-size-md);
color: var(--text-muted);
font-size: 1rem;
color: #6b7280;
}
/* Expanded card styles */
@@ -117,6 +144,27 @@
margin-bottom: 1.5rem;
}
.readOnlySection {
margin-bottom: 1.5rem;
}
.readOnlyField {
margin-bottom: 1rem;
}
.readOnlyField:last-child {
margin-bottom: 0;
}
.createdByText,
.participantsText,
.timeText {
margin: 0;
font-size: var(--font-size-base);
color: var(--text-primary);
font-weight: var(--font-weight-medium);
}
.editSection {
margin-bottom: 1.5rem;
}

View File

@@ -15,10 +15,31 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
return dateA - dateB;
});
// Group bookings by date
const groupedBookings = sortedBookings.reduce((groups, booking) => {
const dateKey = `${booking.date.year}-${booking.date.month}-${booking.date.day}`;
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(booking);
return groups;
}, {});
function handleBookingClick(booking) {
setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id);
}
function formatDateHeader(dateObj) {
const days = ['SÖNDAG', 'MÅNDAG', 'TISDAG', 'ONSDAG', 'TORSDAG', 'FREDAG', 'LÖRDAG'];
const months = ['JANUARI', 'FEBRUARI', 'MARS', 'APRIL', 'MAJ', 'JUNI', 'JULI', 'AUGUSTI', 'SEPTEMBER', 'OKTOBER', 'NOVEMBER', 'DECEMBER'];
const date = new Date(dateObj.year, dateObj.month - 1, dateObj.day);
const dayName = days[date.getDay()];
const monthName = months[dateObj.month - 1];
return `${dayName} ${dateObj.day} ${monthName} ${dateObj.year}`;
}
return (
<div className={styles.bookingsListContainer}>
{showSuccessBanner && (
@@ -71,17 +92,24 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
/>
)}
<div className={styles.bookingsContainer}>
{sortedBookings.length > 0 ? (
{Object.keys(groupedBookings).length > 0 ? (
<>
{sortedBookings.map((booking, index) => (
<BookingCard
key={index}
booking={booking}
onClick={() => handleBookingClick(booking)}
isExpanded={expandedBookingId === booking.id}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
/>
{Object.entries(groupedBookings).map(([dateKey, dayBookings]) => (
<div key={dateKey} className={styles.dateGroup}>
<h2 className={styles.dateHeader}>
{formatDateHeader(dayBookings[0].date)}
</h2>
{dayBookings.map((booking, index) => (
<BookingCard
key={booking.id}
booking={booking}
onClick={() => handleBookingClick(booking)}
isExpanded={expandedBookingId === booking.id}
onBookingUpdate={onBookingUpdate}
onBookingDelete={onBookingDelete}
/>
))}
</div>
))}
</>
) : (

View File

@@ -63,4 +63,22 @@
opacity: 1;
transform: translateY(0);
}
}
.dateGroup {
margin-bottom: 2rem;
}
.dateGroup:last-child {
margin-bottom: 0;
}
.dateHeader {
font-size: 0.875rem;
font-weight: 600;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 0.25rem 0;
padding: 0;
}