-
-
+ <>
+
+
+
+
+ {isExpanded && effectiveEditMode === 'inline' && !bookingState.isRoomInfoModalOpen && (
+
+ {renderExpandedContent()}
-
-
-
-
- Ändra längd
-
+ actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ setActiveView={bookingState.setActiveView}
/>
-
- {!showDeleteConfirm ? (
-
-
- Radera
-
-
- Avbryt
-
-
- {selectedLength !== null ? 'Spara ändringar' : 'Välj längd först'}
-
-
- ) : (
-
-
-
⚠️
-
Är du säker på att du vill radera denna bokning?
-
- "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)}
-
-
-
-
- Ja, radera
-
-
- Avbryt
-
-
-
- )}
+ )}
+
+
+ {isParticipantBooking && booking.createdBy && !isExpanded && (
+
+ Tillagd av {booking.createdBy.name}
-
- )}
-
+ )}
+
+
+ {/* Room information modal */}
+
{
+ bookingState.setIsRoomInfoModalOpen(false);
+ bookingState.setActiveView('closed');
+ }}
+ title="Lokalinformation"
+ booking={booking}
+ calculatedEndTime={bookingState.calculatedEndTime}
+ >
+
+
+
+ {/* Full edit modal - shown after selecting "Hantera bokning" */}
+
{
+ setIsModalOpen(false);
+ bookingState.setActiveView('closed');
+ }}
+ title={isParticipantBooking ? "Visa bokning" : "Redigera bokning"}
+ booking={booking}
+ calculatedEndTime={bookingState.calculatedEndTime}
+ >
+ {renderExpandedContent(true)}
+
+ >
);
}
diff --git a/my-app/src/components/booking/BookingCard.module.css b/my-app/src/components/booking/BookingCard.module.css
index 09f3df7..da96e6e 100644
--- a/my-app/src/components/booking/BookingCard.module.css
+++ b/my-app/src/components/booking/BookingCard.module.css
@@ -1,56 +1,107 @@
-.card {
- border: var(--border-width-thin) solid var(--border-light);
- padding: var(--card-padding);
+.cardWrapper {
width: 100%;
- border-radius: var(--border-radius-md);
- background: var(--bg-primary);
transition: var(--transition-medium);
- box-shadow: var(--shadow-lg);
+ /*max-width: 400px;*/
+ /*flex: 1;*/
}
-.card:hover {
- cursor: pointer;
- border-color: var(--color-primary);
- box-shadow: var(--shadow-xl);
- /*transform: translateY(-2px);*/
+.card {
+ border: var(--border-width-thin) solid var(--border-light);
+ padding: 0.8rem 1rem;
+ width: 100%;
+ background: var(--bg-primary);
+ /*transition: var(--transition-medium);*/
}
+@media (hover: hover) {
+ .cardWrapper:hover:not(.expanded) .card {
+ cursor: pointer;
+ border-color: var(--border-medium);
+ /*box-shadow: var(--shadow-xl);*/
+ /*transform: translateY(-2px);*/
+ }
+}
+
+.banner {
+ background-color: var(--bg-secondary);
+ color: #6b7280;
+ font-size: 0.8rem;
+ padding: 0.25rem 1rem;
+ border-left: var(--border-width-thin) solid var(--border-light);
+ border-right: var(--border-width-thin) solid var(--border-light);
+ border-bottom: var(--border-width-thin) solid var(--border-light);
+ /*border-bottom-left-radius: var(--border-radius-md);
+ border-bottom-right-radius: var(--border-radius-md);*/
+}
+
+
.header {
display: flex;
- justify-content: space-between;
- align-items: center;
- height: 100%;
+ flex-direction: column;
+ width: 100%;
}
-.leftSection {
- flex: 1;
+.cardWrapper:not(.expanded) .header {
+ cursor: pointer;
}
-.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 {
+.topSection {
display: flex;
- align-items: center;
- gap: var(--spacing-md);
- margin-bottom: var(--spacing-sm);
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-end;
+ /*background-color: lightblue;*/
+}
+
+.bottomSection {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-end;
+ width: 100%;
+ /*background-color: lightgreen;*/
+}
+
+.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;
+}
+
+.participants,
+.createdBy {
+ margin: 0;
+ font-size: 1rem;
+ color: #6b7280;
+ padding: 0.25rem 0;
+}
+
+.roomBadge {
+ background-color: var(--su-blue);
+ 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);
@@ -65,32 +116,8 @@
gap: var(--spacing-lg);
}
-.timeSection {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 0.2rem;
-}
-
-.startTime {
- font-size: 1.6rem;
- font-weight: var(--font-weight-normal);
+.time {
color: var(--text-primary);
- line-height: 1;
-}
-
-.endTime {
- font-size: 1.6rem;
- font-weight: var(--font-weight-normal);
- color: var(--text-tertiary);
- line-height: 1;
-}
-
-.participants {
- margin: 0;
- font-size: var(--font-size-md);
- color: var(--text-muted);
}
/* Expanded card styles */
@@ -99,6 +126,12 @@
box-shadow: var(--shadow-xl);
}
+.expanded .card {
+ outline: 2px solid #3b82f6;
+ outline-offset: -1px;
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
+}
+
.expanded:hover {
transform: none;
}
@@ -108,15 +141,43 @@
}
.expandedContent {
- margin-top: 1.5rem;
- padding-top: 1.5rem;
+ padding-top:1rem;
+ margin-top:1rem;
border-top: 1px solid #E5E5E5;
}
+.divider {
+ border: none;
+ border-top: 1px solid var(--border-medium);
+ margin: 1rem 0;
+ margin-bottom: 1.5rem;
+}
+
.formSection {
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;
}
@@ -126,7 +187,17 @@
font-size: 0.8rem;
color: #717171;
font-weight: 500;
- margin-bottom: 0.5rem;
+ /*margin-bottom: 0.5rem;*/
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.compactElementHeading {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ font-weight: 500;
+ margin-bottom: 0.4rem;
+ margin-top: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -164,11 +235,11 @@
.cancelButton {
flex: 2;
- background-color: white;
+ background-color: var(--bg-primary);
height: 3rem;
- color: #374151;
+ color: var(--text-primary);
font-weight: 600;
- border: 2px solid #d1d5db;
+ border: 1px solid var(--border-light);
border-radius: 0.5rem;
transition: all 0.2s ease;
cursor: pointer;
@@ -176,12 +247,12 @@
}
.cancelButton:hover {
- background-color: #f9fafb;
- border-color: #9ca3af;
+ background-color: var(--bg-secondary);
+ border-color: var(--border-medium);
}
.cancelButton:active {
- background-color: #e5e7eb;
+ background-color: var(--bg-tertiary);
transform: translateY(1px);
}
@@ -461,4 +532,116 @@
.cancelDeleteButton[data-focused] {
outline: 2px solid #2563EB;
outline-offset: -1px;
+}
+
+/* Options expanded card styles */
+.optionsExpanded {
+ border-color: var(--color-primary);
+ box-shadow: var(--shadow-xl);
+}
+
+.optionsExpanded .card {
+ outline: 2px solid #3b82f6;
+ outline-offset: -1px;
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
+}
+
+.optionsExpanded:hover {
+ transform: none;
+}
+
+.optionsExpanded .header {
+ cursor: default;
+}
+
+/* Inline options accordion styles */
+.optionsContent {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--border-light);
+ animation: slideDown 0.2s ease-out;
+}
+
+@keyframes slideDown {
+ from {
+ max-height: 0;
+ opacity: 0;
+ }
+ to {
+ max-height: 200px;
+ opacity: 1;
+ }
+}
+
+
+
+
+/* Room information styles */
+.roomInfoContent {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.roomImageContainer {
+ width: 100%;
+ overflow: hidden;
+ background-color: var(--bg-secondary);
+}
+
+.roomImage {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ background-color: #f3f4f6;
+}
+
+.roomDetails {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.roomTitle {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.roomInfoGrid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.roomInfoItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--border-light);
+}
+
+.roomInfoItem:last-child {
+ border-bottom: none;
+}
+
+.roomInfoLabel {
+ font-weight: 500;
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.roomInfoValue {
+ color: var(--text-primary);
+ text-align: right;
+ margin-left: 1rem;
+}
+
+.roomInfoActions {
+ margin-top: 1.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--border-light);
+ display: flex;
+ justify-content: center;
}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardHeader.jsx b/my-app/src/components/booking/BookingCardHeader.jsx
new file mode 100644
index 0000000..a0728cf
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardHeader.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import styles from './BookingCard.module.css';
+import { getTimeFromIndex } from '../../utils/bookingUtils';
+import { ParticipantsDisplay } from './ParticipantsDisplay';
+
+export function BookingCardHeader({
+ booking,
+ calculatedEndTime,
+ isExpanded,
+ activeView,
+ onOptionsToggle,
+ onClick
+}) {
+ const handleClick = !isExpanded ? onOptionsToggle : (activeView === 'closed' ? onClick : undefined);
+
+ return (
+
+
+
+
{booking.title}
+
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)}
+
+
+
+ {booking.participants && booking.participants.length > 0 && (
+
+ )}
+
+ {booking.room}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardModal.jsx b/my-app/src/components/booking/BookingCardModal.jsx
new file mode 100644
index 0000000..cc7a627
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardModal.jsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { Button, Dialog, Heading, Modal } from 'react-aria-components';
+import { convertDateObjectToString } from '../../helpers';
+import styles from './BookingCardModal.module.css';
+
+export function BookingCardModal({
+ isOpen,
+ onClose,
+ children,
+ title = "Redigera bokning",
+ booking,
+ calculatedEndTime
+}) {
+ function getTimeFromIndex(timeIndex) {
+ const totalHalfHoursFromStart = timeIndex;
+ const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base
+
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+
+ return `${hours}:${minutes === 0 ? '00' : '30'}`;
+ }
+ return (
+
!open && onClose && onClose()}
+ className={styles.modal}
+ >
+
+
+
+
{booking.title}
+ {booking && (
+
+
+ {convertDateObjectToString(booking.date)}
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(calculatedEndTime || booking.endTime)}
+
+ {booking.room}
+
+
+ )}
+
+
+ ×
+
+
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardModal.module.css b/my-app/src/components/booking/BookingCardModal.module.css
new file mode 100644
index 0000000..e839d1f
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardModal.module.css
@@ -0,0 +1,127 @@
+.modal {
+ --overlay-background: transparent;
+ padding: 0;
+ z-index: 1000;
+ position: relative;
+}
+
+.dialog {
+ background: var(--bg-primary);
+ border-radius: 0.75rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ max-width: 500px;
+ width: 100%;
+ max-height: 90vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 1.5rem 1.5rem 0;
+ border-bottom: 1px solid var(--border-light);
+ padding-bottom: 1rem;
+}
+
+.titleSection {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.title {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.bookingInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.bookingTitle {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.bookingDetails {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.date {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.time {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.room {
+ background-color: var(--su-blue);
+ color: white;
+ padding: 0.2rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.closeButton {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ border-radius: 0.375rem;
+ line-height: 1;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+}
+
+.closeButton:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.content {
+ padding: 1.5rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+@media (max-width: 768px) {
+ .modal {
+ padding: 0.5rem;
+ }
+
+ .dialog {
+ max-height: 95vh;
+ }
+
+ .header {
+ padding: 1rem 1rem 0;
+ padding-bottom: 0.75rem;
+ }
+
+ .content {
+ padding: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardRefactored.jsx b/my-app/src/components/booking/BookingCardRefactored.jsx
new file mode 100644
index 0000000..88e3c97
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardRefactored.jsx
@@ -0,0 +1,160 @@
+import React, { useState } from 'react';
+import { BookingProvider } from '../../context/BookingContext';
+import { PEOPLE } from '../../constants/bookingConstants';
+
+import { useBookingCardState } from '../../hooks/useBookingCardState';
+import { useBookingActions } from '../../hooks/useBookingActions';
+import { useResponsiveMode } from '../../hooks/useResponsiveMode';
+
+import { BookingCardHeader } from './BookingCardHeader';
+import { BookingCardTabs } from './BookingCardTabs';
+import { RoomInfoContent } from './RoomInfoContent';
+import { BookingFormContent } from './BookingFormContent';
+import { ParticipantBookingContent } from './ParticipantBookingContent';
+import { BookingCardModal } from './BookingCardModal';
+
+import styles from './BookingCard.module.css';
+
+function BookingCard({
+ booking,
+ onClick,
+ isExpanded,
+ onBookingUpdate,
+ onBookingDelete,
+ editMode = 'inline',
+ isOptionsExpanded,
+ onOptionsToggle
+}) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const isParticipantBooking = booking.isParticipantBooking === true;
+
+ // Custom hooks
+ const bookingState = useBookingCardState(booking, isExpanded, isModalOpen);
+ const { effectiveEditMode } = useResponsiveMode(editMode, isExpanded, isModalOpen, onClick, setIsModalOpen);
+ const actions = useBookingActions(booking, bookingState, onBookingUpdate, onBookingDelete, onClick, effectiveEditMode, setIsModalOpen);
+
+ // Create a local booking context for the components
+ const localBookingContext = {
+ title: bookingState.editedTitle,
+ setTitle: bookingState.setEditedTitle,
+ participants: bookingState.editedParticipants,
+ handleParticipantChange: actions.handleParticipantChange,
+ handleRemoveParticipant: actions.handleRemoveParticipant
+ };
+
+ // Render the expanded content
+ const renderExpandedContent = () => (
+
+ actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ setActiveView={bookingState.setActiveView}
+ />
+
+ {bookingState.activeView === 'lokalinfo' ? (
+ {
+ bookingState.setActiveView('hantera');
+ onClick();
+ }}
+ />
+ ) : bookingState.activeView === 'hantera' ? (
+ isParticipantBooking ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+ );
+
+ return (
+ <>
+
+
+
+
+ {isExpanded && effectiveEditMode === 'inline' && !bookingState.isRoomInfoModalOpen && (
+
+ {renderExpandedContent()}
+
+ )}
+
+ {isOptionsExpanded && !isExpanded && (
+
+ actions.handleRoomInfo(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ onManageBooking={() => actions.handleManageBooking(effectiveEditMode, isOptionsExpanded, onOptionsToggle, isExpanded)}
+ setActiveView={bookingState.setActiveView}
+ />
+
+ )}
+
+
+ {isParticipantBooking && booking.createdBy && !isExpanded && (
+
+ Tillagd av {booking.createdBy.name}
+
+ )}
+
+
+ {/* Room information modal */}
+
{
+ bookingState.setIsRoomInfoModalOpen(false);
+ bookingState.setActiveView('hantera');
+ }}
+ title="Lokalinformation"
+ booking={booking}
+ calculatedEndTime={bookingState.calculatedEndTime}
+ >
+
+
+
+ {/* Full edit modal - shown after selecting "Hantera bokning" */}
+
setIsModalOpen(false)}
+ title={isParticipantBooking ? "Visa bokning" : "Redigera bokning"}
+ booking={booking}
+ calculatedEndTime={bookingState.calculatedEndTime}
+ >
+ {renderExpandedContent()}
+
+ >
+ );
+}
+
+export default BookingCard;
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardTabs.jsx b/my-app/src/components/booking/BookingCardTabs.jsx
new file mode 100644
index 0000000..99282a8
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardTabs.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Button } from 'react-aria-components';
+import styles from './BookingCardTabs.module.css';
+
+export function BookingCardTabs({
+ activeView,
+ isInExpandedView = false,
+ onRoomInfo,
+ onManageBooking,
+ setActiveView
+}) {
+ const containerClass = isInExpandedView && activeView !== 'closed'
+ ? styles.tabButtons
+ : styles.tabButtonsNoBorder;
+
+ return (
+
+ {
+ // Remove focus on touch devices to prevent persistent styling
+ if (e.target) e.target.blur();
+
+ if (isInExpandedView && activeView === 'lokalinfo') {
+ // Close content, show only tab buttons
+ setActiveView('closed');
+ } else {
+ onRoomInfo();
+ }
+ }}
+ >
+ Lokalinformation
+
+ {
+ // Remove focus on touch devices to prevent persistent styling
+ if (e.target) e.target.blur();
+
+ if (isInExpandedView && activeView === 'hantera') {
+ // Close content, show only tab buttons
+ setActiveView('closed');
+ } else {
+ onManageBooking();
+ }
+ }}
+ >
+ Hantera bokning
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingCardTabs.module.css b/my-app/src/components/booking/BookingCardTabs.module.css
new file mode 100644
index 0000000..d29b198
--- /dev/null
+++ b/my-app/src/components/booking/BookingCardTabs.module.css
@@ -0,0 +1,86 @@
+/* Container styles */
+.tabButtons {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid var(--border-light);
+ padding-bottom: 1rem;
+}
+
+.tabButtonsNoBorder {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+}
+
+/* Button styles */
+.tabButton {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-light);
+ color: var(--text-primary);
+ padding: 0.75rem 1rem;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: center;
+ border-radius: var(--border-radius-sm);
+ flex: 1;
+}
+
+.tabButton:hover {
+ border-color: var(--color-primary);
+ background-color: var(--bg-primary);
+}
+
+.tabButton:focus {
+ outline: none;
+}
+
+.tabButton:focus-visible {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+.tabButton:active {
+ background-color: var(--bg-secondary);
+ transform: none;
+}
+
+/* Touch device specific styles */
+@media (hover: none) {
+ .tabButton:hover {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-light);
+ }
+
+ .tabButton:focus {
+ outline: none;
+ background-color: var(--bg-secondary);
+ }
+
+ .tabButton:focus-visible {
+ outline: none;
+ }
+
+ .activeTab:hover {
+ background: #6b7280 !important;
+ color: white !important;
+ border-color: #4b5563 !important;
+ }
+}
+
+/* Active tab styling */
+.activeTab {
+ background: #6b7280 !important;
+ color: white !important;
+ border-color: #4b5563 !important;
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.activeTab:hover {
+ background: #6b7280 !important;
+ color: white !important;
+ border-color: #4b5563 !important;
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingFormContent.jsx b/my-app/src/components/booking/BookingFormContent.jsx
new file mode 100644
index 0000000..ff343c2
--- /dev/null
+++ b/my-app/src/components/booking/BookingFormContent.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { Button } from 'react-aria-components';
+import styles from './BookingCard.module.css';
+import Dropdown from '../ui/Dropdown';
+import { BookingTitleField } from '../forms/BookingTitleField';
+import { ParticipantsSelector } from '../forms/ParticipantsSelector';
+import { Label } from '../ui/Label';
+import { ConfirmationDialog } from './ConfirmationDialog';
+import { createBookingLengthOptions } from '../../utils/bookingUtils';
+
+export function BookingFormContent({
+ booking,
+ calculatedEndTime,
+ showDeleteConfirm,
+ hasChanges,
+ onLengthChange,
+ onSave,
+ onCancel,
+ onDelete,
+ onConfirmDelete,
+ onCancelDelete
+}) {
+ const bookingLengths = createBookingLengthOptions(booking);
+ const disabledOptions = {};
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Ändra längd
+
+
+
+
+
+ {!showDeleteConfirm ? (
+
+
+ Radera
+
+
+ Avbryt
+
+
+ Spara
+
+
+ ) : (
+
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingOptionsModal.jsx b/my-app/src/components/booking/BookingOptionsModal.jsx
new file mode 100644
index 0000000..6ae8208
--- /dev/null
+++ b/my-app/src/components/booking/BookingOptionsModal.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Button, Dialog, Heading, Modal } from 'react-aria-components';
+import { convertDateObjectToString } from '../../helpers';
+import styles from './BookingOptionsModal.module.css';
+
+export function BookingOptionsModal({
+ isOpen,
+ onClose,
+ booking,
+ onRoomInfo,
+ onManageBooking
+}) {
+ function getTimeFromIndex(timeIndex) {
+ const totalHalfHoursFromStart = timeIndex;
+ const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base
+
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+
+ return `${hours}:${minutes === 0 ? '00' : '30'}`;
+ }
+
+ return (
+
!open && onClose && onClose()}
+ className={styles.modal}
+ >
+
+
+
+
{booking?.title}
+ {booking && (
+
+
+ {convertDateObjectToString(booking.date)}
+
+ {getTimeFromIndex(booking.startTime)} – {getTimeFromIndex(booking.endTime)}
+
+ {booking.room}
+
+
+ )}
+
+
+ ×
+
+
+
+
+
+ Lokalinformation
+
+
+ Hantera bokning
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingOptionsModal.module.css b/my-app/src/components/booking/BookingOptionsModal.module.css
new file mode 100644
index 0000000..e15f9c8
--- /dev/null
+++ b/my-app/src/components/booking/BookingOptionsModal.module.css
@@ -0,0 +1,144 @@
+.modal {
+ --overlay-background: transparent;
+ padding: 0;
+ z-index: 1000;
+ position: relative;
+}
+
+.dialog {
+ background: var(--bg-primary);
+ border-radius: 0.75rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ max-width: 400px;
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 1.5rem 1.5rem 0;
+ border-bottom: 1px solid var(--border-light);
+ padding-bottom: 1rem;
+}
+
+.titleSection {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.title {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.bookingInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.bookingDetails {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.date {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.time {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.room {
+ background-color: var(--su-blue);
+ color: white;
+ padding: 0.15rem 0.4rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.closeButton {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ border-radius: 0.375rem;
+ line-height: 1;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ flex-shrink: 0;
+}
+
+.closeButton:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.content {
+ padding: 1.5rem;
+}
+
+.buttonGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.optionButton {
+ background: var(--bg-primary);
+ border: 2px solid var(--border-light);
+ color: var(--text-primary);
+ padding: 1rem 1.5rem;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: center;
+}
+
+.optionButton:hover {
+ border-color: var(--color-primary);
+ background-color: var(--bg-secondary);
+}
+
+.optionButton:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+@media (max-width: 768px) {
+ .modal {
+ padding: 0.5rem;
+ }
+
+ .header {
+ padding: 1rem 1rem 0;
+ padding-bottom: 0.75rem;
+ }
+
+ .content {
+ padding: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/BookingsList.jsx b/my-app/src/components/booking/BookingsList.jsx
index 369c426..9d66930 100644
--- a/my-app/src/components/booking/BookingsList.jsx
+++ b/my-app/src/components/booking/BookingsList.jsx
@@ -3,9 +3,12 @@ import { CalendarDate } from '@internationalized/date';
import styles from './BookingsList.module.css';
import BookingCard from './BookingCard';
import NotificationBanner from '../common/NotificationBanner';
+import { useSettingsContext } from '../../context/SettingsContext';
-function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) {
+function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showLeaveBanner, lastLeftBooking, onDismissLeaveBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner, showUpdateBanner, lastUpdatedBooking, onDismissUpdateBanner }) {
+ const { settings } = useSettingsContext();
const [expandedBookingId, setExpandedBookingId] = useState(null);
+ const [optionsExpandedBookingId, setOptionsExpandedBookingId] = useState(null);
// Sort bookings by date (earliest first)
const sortedBookings = [...bookings].sort((a, b) => {
@@ -15,10 +18,45 @@ 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);
+ console.log("HEY:");
+ console.log(groups);
+ return groups;
+ }, {});
+
+ const groupedBookingsArray = Object.values(groupedBookings);
+ console.log("GROUPED BOOKINGS ARRAY:");
+ console.log(groupedBookingsArray);
+
function handleBookingClick(booking) {
+ // Close any expanded options accordion when expanding a card
+ setOptionsExpandedBookingId(null);
setExpandedBookingId(expandedBookingId === booking.id ? null : booking.id);
}
+ function handleOptionsToggle(booking) {
+ // Close any expanded card when opening options accordion
+ setExpandedBookingId(null);
+ setOptionsExpandedBookingId(optionsExpandedBookingId === 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 (
{showSuccessBanner && (
@@ -37,6 +75,22 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
showCloseButton={true}
/>
)}
+ {showLeaveBanner && (
+
+ )}
+ {showUpdateBanner && (
+
+ )}
{showDevelopmentBanner && (
)}
- {sortedBookings.length > 0 ? (
+ {Object.keys(groupedBookings).length > 0 ? (
<>
- {sortedBookings.map((booking, index) => (
-
handleBookingClick(booking)}
- isExpanded={expandedBookingId === booking.id}
- onBookingUpdate={onBookingUpdate}
- onBookingDelete={onBookingDelete}
- />
+ {Object.entries(groupedBookings).map(([dateKey, dayBookings]) => (
+
+
+ {formatDateHeader(dayBookings[0].date)}
+
+ {dayBookings.map((booking, index) => (
+ handleBookingClick(booking)}
+ isExpanded={expandedBookingId === booking.id}
+ onBookingUpdate={onBookingUpdate}
+ onBookingDelete={onBookingDelete}
+ editMode={settings.bookingCardEditMode}
+ isOptionsExpanded={optionsExpandedBookingId === booking.id}
+ onOptionsToggle={() => handleOptionsToggle(booking)}
+ />
+ ))}
+
))}
>
) : (
diff --git a/my-app/src/components/booking/BookingsList.module.css b/my-app/src/components/booking/BookingsList.module.css
index 7910180..e7b3337 100644
--- a/my-app/src/components/booking/BookingsList.module.css
+++ b/my-app/src/components/booking/BookingsList.module.css
@@ -1,13 +1,16 @@
.bookingsListContainer {
padding-bottom: var(--spacing-3xl);
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ flex-wrap: wrap;
}
.bookingsContainer {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ flex-wrap: wrap;
+ column-gap: 2rem;
}
.message {
@@ -63,4 +66,39 @@
opacity: 1;
transform: translateY(0);
}
+}
+
+.dateGroup {
+ margin-bottom: 2rem;
+ flex: 1;
+ min-width: 250px;
+ max-width: 500px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.dateGroup:last-child {
+ margin-bottom: 0;
+}
+
+.dateHeader {
+ width: fit-content;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #9ca3af;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 0 0.25rem 0;
+ padding: 0;
+}
+
+@media screen and (max-width: 768px) {
+ .bookingsContainer {
+ grid-template-columns: 1fr;
+ }
+
+ .dateGroup {
+ max-width: 100%;
+ }
}
\ No newline at end of file
diff --git a/my-app/src/components/booking/ConfirmationDialog.jsx b/my-app/src/components/booking/ConfirmationDialog.jsx
new file mode 100644
index 0000000..5e96d8b
--- /dev/null
+++ b/my-app/src/components/booking/ConfirmationDialog.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { Button } from 'react-aria-components';
+import styles from './BookingCard.module.css';
+import { getTimeFromIndex } from '../../utils/bookingUtils';
+
+export function ConfirmationDialog({
+ booking,
+ isParticipantBooking = false,
+ onConfirm,
+ onCancel
+}) {
+ const isLeaveDialog = isParticipantBooking;
+
+ return (
+
+
+
⚠️
+
+ {isLeaveDialog
+ ? "Är du säker på att du vill lämna denna bokning?"
+ : "Är du säker på att du vill radera denna bokning?"
+ }
+
+
+ {isLeaveDialog
+ ? `Du kommer inte längre att vara med på "${booking.title}" den ${booking.date.day}/${booking.date.month}`
+ : `"${booking.title}" den ${booking.date.day}/${booking.date.month} kl. ${getTimeFromIndex(booking.startTime)}`
+ }
+
+
+
+
+ {isLeaveDialog ? "Ja, lämna bokning" : "Ja, radera"}
+
+
+ Avbryt
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/components/booking/InlineModalBookingForm.module.css b/my-app/src/components/booking/InlineModalBookingForm.module.css
index 380b380..0a05075 100644
--- a/my-app/src/components/booking/InlineModalBookingForm.module.css
+++ b/my-app/src/components/booking/InlineModalBookingForm.module.css
@@ -8,11 +8,11 @@
animation: slideDown 0.2s ease-out;
width: 100%;
flex-basis: 100%;
- max-width: none;
position: relative;
z-index: 1;
}
+
/* Arrow pointing to left card */
.arrowLeft::before {
content: '';
diff --git a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx
index 23c29db..ab67a05 100644
--- a/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx
+++ b/my-app/src/components/booking/InlineModalExtendedBookingForm.jsx
@@ -9,7 +9,6 @@ import { ParticipantsSelector } from '../forms/ParticipantsSelector';
import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import { generateId } from '../../utils/bookingUtils';
-import { USER } from '../../constants/bookingConstants';
import styles from './InlineModalBookingForm.module.css';
import extendedStyles from './InlineModalExtendedBookingForm.module.css';
@@ -41,6 +40,7 @@ export function InlineModalExtendedBookingForm({
const navigate = useNavigate();
const booking = useBookingContext();
const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext();
+ const currentUser = getCurrentUser();
// Initialize with pre-selected end time if available, or auto-select if only 30 min available
const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength :
@@ -144,9 +144,12 @@ export function InlineModalExtendedBookingForm({
// Check if user has selected an end time (including pre-selected)
const hasSelectedEndTime = selectedEndTimeIndex !== null;
+
+ // Check if user has added at least one additional participant (beyond themselves)
+ const hasRequiredParticipants = booking.participants.length > 0;
const handleSave = () => {
- if (hasSelectedEndTime && addBooking) {
+ if (hasSelectedEndTime && hasRequiredParticipants && addBooking) {
console.log('Booking context state:', {
title: booking.title,
participants: booking.participants,
@@ -158,9 +161,9 @@ export function InlineModalExtendedBookingForm({
const roomToBook = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom;
// Include the current user as a participant if not already added
- const allParticipants = booking.participants.find(p => p.id === USER.id)
+ const allParticipants = booking.participants.find(p => p.id === currentUser.id)
? booking.participants
- : [USER, ...booking.participants];
+ : [currentUser, ...booking.participants];
const finalTitle = booking.title !== "" ? booking.title : getDefaultBookingTitle();
@@ -201,9 +204,9 @@ export function InlineModalExtendedBookingForm({
{(() => {
// Include the current user as a participant if not already added
- const allParticipants = booking.participants.find(p => p.id === USER.id)
+ const allParticipants = booking.participants.find(p => p.id === currentUser.id)
? booking.participants
- : [USER, ...booking.participants];
+ : [currentUser, ...booking.participants];
const startTime = getTimeFromIndex(booking.selectedStartIndex);
const endTime = getTimeFromIndex(booking.selectedEndIndex);
@@ -306,6 +309,15 @@ export function InlineModalExtendedBookingForm({
{/* Participants Field - Compact */}
+ {/*!hasRequiredParticipants && (
+
+ 💡 Lägg till minst en deltagare
+
+ )*/}
@@ -317,11 +329,13 @@ export function InlineModalExtendedBookingForm({
Avbryt
- {hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}
+ {!hasSelectedEndTime ? 'Välj sluttid först' :
+ !hasRequiredParticipants ? 'Minst en till deltagare krävs' :
+ 'Boka'}
diff --git a/my-app/src/components/booking/ParticipantBookingContent.jsx b/my-app/src/components/booking/ParticipantBookingContent.jsx
new file mode 100644
index 0000000..bf0c4be
--- /dev/null
+++ b/my-app/src/components/booking/ParticipantBookingContent.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Button } from 'react-aria-components';
+import styles from './BookingCard.module.css';
+import { Label } from '../ui/Label';
+import { ConfirmationDialog } from './ConfirmationDialog';
+
+export function ParticipantBookingContent({
+ booking,
+ showDeleteConfirm,
+ onRemoveSelf,
+ onCancel,
+ onSetShowDeleteConfirm,
+ onCancelDelete
+}) {
+ return (
+ <>
+