From 787897bab62f42962ce080614863c9714c5c92a1 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:26:13 +0200 Subject: [PATCH 01/41] first --- my-app/src/App.jsx | 1 + my-app/src/AppRoutes.jsx | 1 + my-app/src/Layout.jsx | 1 + my-app/src/components/Booking.jsx | 1 + my-app/src/components/BookingDatePicker.jsx | 1 + my-app/src/components/BookingFormFields.jsx | 1 + my-app/src/components/BookingsList.jsx | 1 + my-app/src/components/Card.jsx | 1 + my-app/src/components/Dropdown.jsx | 1 + my-app/src/components/TimeCard.jsx | 2 +- my-app/src/components/TimeCardContainer.jsx | 1 + my-app/src/context/BookingContext.jsx | 2 +- my-app/src/main.jsx | 2 +- my-app/src/pages/NewBooking.jsx | 1 + my-app/src/pages/RoomBooking.jsx | 1 + my-app/vite.config.js | 39 +++------------------ 16 files changed, 19 insertions(+), 38 deletions(-) diff --git a/my-app/src/App.jsx b/my-app/src/App.jsx index 5ec76de..af9cdd4 100644 --- a/my-app/src/App.jsx +++ b/my-app/src/App.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import AppRoutes from './AppRoutes'; // move the routing and loading logic here diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 5c7189f..eec5074 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Routes, Route, useLocation } from 'react-router-dom'; import { useEffect, useState } from 'react'; import Layout from './Layout'; diff --git a/my-app/src/Layout.jsx b/my-app/src/Layout.jsx index 76c608f..8b4bf89 100644 --- a/my-app/src/Layout.jsx +++ b/my-app/src/Layout.jsx @@ -1,4 +1,5 @@ // components/Layout.jsx +import React from 'react'; import { Outlet, Link } from 'react-router-dom'; import Header from './components/Header'; diff --git a/my-app/src/components/Booking.jsx b/my-app/src/components/Booking.jsx index f5ab816..e88f286 100644 --- a/my-app/src/components/Booking.jsx +++ b/my-app/src/components/Booking.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './Booking.module.css'; import { convertDateObjectToString } from '../helpers'; diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index 516c003..e926d76 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { DatePicker } from '../react-aria-starter/src/DatePicker'; import { today, getLocalTimeZone } from '@internationalized/date'; import { getFutureDate, isDateUnavailable } from '../utils/bookingUtils'; diff --git a/my-app/src/components/BookingFormFields.jsx b/my-app/src/components/BookingFormFields.jsx index 1f5e6de..7332d60 100644 --- a/my-app/src/components/BookingFormFields.jsx +++ b/my-app/src/components/BookingFormFields.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import Dropdown from './Dropdown'; import { ComboBox } from '../react-aria-starter/src/ComboBox'; import { DEFAULT_BOOKING_TITLE, BOOKING_LENGTHS, SMALL_GROUP_ROOMS, PEOPLE } from '../constants/bookingConstants'; diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index b489bb2..883fb89 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './BookingsList.module.css'; import Booking from './Booking'; diff --git a/my-app/src/components/Card.jsx b/my-app/src/components/Card.jsx index 940676e..7eca5cc 100644 --- a/my-app/src/components/Card.jsx +++ b/my-app/src/components/Card.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './Card.module.css'; // Import the CSS Module const Card = ({ imageUrl, header, subheader }) => { diff --git a/my-app/src/components/Dropdown.jsx b/my-app/src/components/Dropdown.jsx index 6bc5dd1..5807b2f 100644 --- a/my-app/src/components/Dropdown.jsx +++ b/my-app/src/components/Dropdown.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from "./Dropdown.module.css"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index 92bbe1e..6f60c10 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -1,6 +1,6 @@ import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components'; -import { useState } from 'react'; +import React, { useState } from 'react'; import styles from './TimeCard.module.css'; diff --git a/my-app/src/components/TimeCardContainer.jsx b/my-app/src/components/TimeCardContainer.jsx index d1c2628..3720478 100644 --- a/my-app/src/components/TimeCardContainer.jsx +++ b/my-app/src/components/TimeCardContainer.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import TimeCard from './TimeCard'; import styles from './TimeCardContainer.module.css'; import { useBookingContext } from '../context/BookingContext'; diff --git a/my-app/src/context/BookingContext.jsx b/my-app/src/context/BookingContext.jsx index b162629..33966c8 100644 --- a/my-app/src/context/BookingContext.jsx +++ b/my-app/src/context/BookingContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import React, { createContext, useContext } from 'react'; const BookingContext = createContext(null); diff --git a/my-app/src/main.jsx b/my-app/src/main.jsx index b9a1a6d..0d0f6e9 100644 --- a/my-app/src/main.jsx +++ b/my-app/src/main.jsx @@ -1,4 +1,4 @@ -import { StrictMode } from 'react' +import React, { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 4f928a5..c34bc39 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/TimeCardContainer'; import { BookingDatePicker } from '../components/BookingDatePicker'; diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 22ff4a1..5732897 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import styles from './RoomBooking.module.css'; import { Link } from 'react-router-dom'; import BookingsList from '../components/BookingsList'; diff --git a/my-app/vite.config.js b/my-app/vite.config.js index cf92bbd..2dea53a 100644 --- a/my-app/vite.config.js +++ b/my-app/vite.config.js @@ -1,38 +1,7 @@ -/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' -// https://vite.dev/config/ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; -const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - -// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - basePath: '/~jare2473', - test: { - projects: [{ - extends: true, - plugins: [ - // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - storybookTest({ - configDir: path.join(dirname, '.storybook') - })], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - provider: 'playwright', - instances: [{ - browser: 'chromium' - }] - }, - setupFiles: ['.storybook/vitest.setup.js'] - } - }] - } -}); \ No newline at end of file +}) \ No newline at end of file -- 2.39.5 From 27174d25adb700d4bcf8527ed5a6ceb4877fdc0c Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:43:54 +0200 Subject: [PATCH 02/41] move combobox to components --- my-app/src/AppRoutes.jsx | 3 +- my-app/src/components/BookingFormFields.jsx | 2 +- .../components/BookingFormFields.module.css | 3 +- my-app/src/components/ComboBox.jsx | 70 ++++++++ my-app/src/components/ComboBox.module.css | 150 ++++++++++++++++++ my-app/src/pages/NewBooking.module.css | 1 + .../src/react-aria-starter/src/ComboBox.css | 2 + 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 my-app/src/components/ComboBox.jsx create mode 100644 my-app/src/components/ComboBox.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index eec5074..d93aa9b 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -24,7 +24,8 @@ const AppRoutes = () => { return ( <> {/* Pass loading as isVisible to FullScreenLoader */} - + {/**/} + }> diff --git a/my-app/src/components/BookingFormFields.jsx b/my-app/src/components/BookingFormFields.jsx index 7332d60..2b1eb48 100644 --- a/my-app/src/components/BookingFormFields.jsx +++ b/my-app/src/components/BookingFormFields.jsx @@ -1,6 +1,6 @@ import React from 'react'; import Dropdown from './Dropdown'; -import { ComboBox } from '../react-aria-starter/src/ComboBox'; +import { ComboBox } from './ComboBox'; import { DEFAULT_BOOKING_TITLE, BOOKING_LENGTHS, SMALL_GROUP_ROOMS, PEOPLE } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; import styles from './BookingFormFields.module.css'; diff --git a/my-app/src/components/BookingFormFields.module.css b/my-app/src/components/BookingFormFields.module.css index c3ec05b..4040478 100644 --- a/my-app/src/components/BookingFormFields.module.css +++ b/my-app/src/components/BookingFormFields.module.css @@ -6,7 +6,8 @@ font-size: 16px; background-color: #FAFBFC; padding: 1rem; - width: 300px; + width: 100%; + max-width: 600px; font-family: inherit; } diff --git a/my-app/src/components/ComboBox.jsx b/my-app/src/components/ComboBox.jsx new file mode 100644 index 0000000..1c88c62 --- /dev/null +++ b/my-app/src/components/ComboBox.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { + Button, + ComboBox as AriaComboBox, + FieldError, + Input, + Label, + ListBox, + ListBoxItem, + Popover, + Text, + ListBoxSection, + Header, + Collection +} from 'react-aria-components'; +import styles from './ComboBox.module.css'; + +export function ComboBox({ + label, + description, + errorMessage, + children, + items, + ...props +}) { + return ( + + +
+ + +
+ {description && {description}} + {errorMessage} + + + +
SENASTE SÖKNINGAR
+ + {item => {item.name}} + +
+
+
+
+ ); +} + +export function ComboBoxItem(props) { + return ; +} + +function UserItem({ children, ...props }) { + return ( + + {({ isSelected }) => ( + <> + + {children} + + {isSelected && ( + + ✓ + + )} + + )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/ComboBox.module.css b/my-app/src/components/ComboBox.module.css new file mode 100644 index 0000000..7aceaac --- /dev/null +++ b/my-app/src/components/ComboBox.module.css @@ -0,0 +1,150 @@ +.comboBoxLabel { + color: var(--text-color); +} + +.comboBoxContainer { + display: flex; + align-items: center; + font-family: inherit; +} + +.comboBoxInput { + margin: 0; + font-size: 1.2rem; + background-color: var(--field-background); + color: var(--field-text-color); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 0.286rem 2rem 0.286rem 0.571rem; + vertical-align: middle; + outline: none; + min-width: 0; + font-family: inherit; + width: 100%; + max-width: 600px; +} + +.comboBoxInput[data-focused] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; +} + +.comboBoxButton { + background: var(--highlight-background); + color: var(--highlight-foreground); + forced-color-adjust: none; + border-radius: 4px; + border: none; + margin-left: -1.714rem; + width: 1.429rem; + height: 1.429rem; + padding: 0; + font-size: 0.857rem; + cursor: default; + flex-shrink: 0; +} + +.comboBoxButton[data-pressed] { + box-shadow: none; + background: var(--highlight-background); +} + +.comboBoxDescription { + font-size: 12px; +} + +.comboBoxError { + font-size: 12px; + color: var(--invalid-color); +} + +.comboBoxPopover[data-trigger="ComboBox"] { + width: var(--trigger-width); +} + +.comboBoxPopover { + box-sizing: border-box; + background-color: white; +} + +.comboBoxList { + display: block; + margin: 0; + padding: 0; + width: unset; + max-height: inherit; + min-height: unset; + border: none; + background-color: white; +} + +.userItem { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: default; + outline: none; + border-radius: 0.125rem; + color: var(--text-color); + padding: 0.5rem; +} + +.userItem[data-focused], +.userItem[data-pressed] { + background: var(--highlight-background); + color: var(--highlight-foreground); +} + +.userItem[data-selected] { + font-weight: 700; + background: unset; + color: var(--text-color); + position: relative; +} + +.userItemContent { + flex: 1; + display: flex; + align-items: center; + gap: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: normal; +} + +.userItem[data-selected] .userItemContent { + font-weight: medium; +} + +.userItemCheckIcon { + width: 1.25rem; + display: flex; + align-items: center; + color: var(--highlight-background); +} + +.userItem[data-focused] .userItemCheckIcon { + color: var(--highlight-foreground); +} + +.comboBoxAvatar { + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: lightgray; +} + +.comboBoxName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sectionHeader { + font-weight: 700; + color: rgb(89, 89, 89); + font-size: 0.9rem; + margin-bottom: 0.4rem; + padding: 0 0.5rem; +} \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index d76fc17..6efba6f 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -1,5 +1,6 @@ .pageContainer { padding: 1rem; + background-color: white; } .formContainer { diff --git a/my-app/src/react-aria-starter/src/ComboBox.css b/my-app/src/react-aria-starter/src/ComboBox.css index 33051f0..c91af2f 100644 --- a/my-app/src/react-aria-starter/src/ComboBox.css +++ b/my-app/src/react-aria-starter/src/ComboBox.css @@ -27,6 +27,8 @@ outline: none; min-width: 0; font-family: inherit; + width: 100%; + max-width: 600px; } .combo-box-input[data-focused] { -- 2.39.5 From 00cf0defe60d5b80f2d2a4a277a6e2bf9c804bef Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:19:42 +0200 Subject: [PATCH 03/41] separate booking form fields into separate components --- my-app/src/components/BookingDatePicker.jsx | 1 - my-app/src/components/BookingLengthField.jsx | 25 ++++++++++++++++ my-app/src/components/BookingTitleField.jsx | 21 ++++++++++++++ my-app/src/components/ParticipantsField.jsx | 29 +++++++++++++++++++ my-app/src/components/RoomSelectionField.jsx | 23 +++++++++++++++ my-app/src/pages/NewBooking.jsx | 30 +++++++++++++------- my-app/src/pages/NewBooking.module.css | 4 +++ 7 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 my-app/src/components/BookingLengthField.jsx create mode 100644 my-app/src/components/BookingTitleField.jsx create mode 100644 my-app/src/components/ParticipantsField.jsx create mode 100644 my-app/src/components/RoomSelectionField.jsx diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index e926d76..57d3579 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -8,7 +8,6 @@ export function BookingDatePicker() { const booking = useBookingContext(); return (
-

Boka rum

+

Längd

+ booking.handleLengthChange(Number(e.target.value))} + placeholder={{ + label: "Alla tider", + value: 0 + }} + disabledOptions={booking.disabledOptions} + /> +
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/BookingTitleField.jsx b/my-app/src/components/BookingTitleField.jsx new file mode 100644 index 0000000..6317ec4 --- /dev/null +++ b/my-app/src/components/BookingTitleField.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './BookingFormFields.module.css'; + +export function BookingTitleField() { + const booking = useBookingContext(); + + return ( + <> +

Titel på bokning

+ booking.setTitle(event.target.value)} + placeholder={DEFAULT_BOOKING_TITLE} + className={styles.textInput} + /> + + ); +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsField.jsx b/my-app/src/components/ParticipantsField.jsx new file mode 100644 index 0000000..57e1c83 --- /dev/null +++ b/my-app/src/components/ParticipantsField.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ComboBox } from './ComboBox'; +import { PEOPLE } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './BookingFormFields.module.css'; + +export function ParticipantsField() { + const booking = useBookingContext(); + + return ( + <> +

Deltagare

+ + + {booking.participants.length > 0 && ( +
+ {booking.participants.map((participant, index) => ( +

{participant}

+ ))} +
+ )} + + ); +} \ No newline at end of file diff --git a/my-app/src/components/RoomSelectionField.jsx b/my-app/src/components/RoomSelectionField.jsx new file mode 100644 index 0000000..37c70d3 --- /dev/null +++ b/my-app/src/components/RoomSelectionField.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Dropdown from './Dropdown'; +import { SMALL_GROUP_ROOMS } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './BookingFormFields.module.css'; + +export function RoomSelectionField() { + const booking = useBookingContext(); + + return ( +
+

Rum

+ booking.handleRoomChange(e)} + placeholder={{ + label: "Alla rum", + value: "allRooms" + }} + /> +
+ ); +} \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index c34bc39..dff3060 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -2,7 +2,10 @@ import React from 'react'; import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/TimeCardContainer'; import { BookingDatePicker } from '../components/BookingDatePicker'; -import { BookingFormFields } from '../components/BookingFormFields'; +import { BookingTitleField } from '../components/BookingTitleField'; +import { ParticipantsField } from '../components/ParticipantsField'; +import { RoomSelectionField } from '../components/RoomSelectionField'; +import { BookingLengthField } from '../components/BookingLengthField'; import { useBookingState } from '../hooks/useBookingState'; import { BookingProvider } from '../context/BookingContext'; @@ -12,18 +15,25 @@ export function NewBooking({ addBooking }) { return (
+

Boka litet grupprum

-
+ + + +
- -
- -

- Lediga tider -

-
- +
+ + +
+ +

+ Lediga tider +

+
+ +
diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index 6efba6f..3e68b45 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -16,6 +16,10 @@ font-weight: 529; } +.bookingTimesContainer { + background-color: red; +} + .modalFooter { width: 100%; -- 2.39.5 From 5cd08fda9fb1d7c19f047e72a8f338f61ab0fec0 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:12:43 +0200 Subject: [PATCH 04/41] chevrons added, kinda works as i want --- my-app/src/icons/ChevronLeft.jsx | 18 +++++++ my-app/src/icons/ChevronRight.jsx | 18 +++++++ my-app/src/pages/NewBooking.module.css | 8 +++- .../src/react-aria-starter/src/DatePicker.css | 47 +++++++++++++++++-- .../src/react-aria-starter/src/DatePicker.tsx | 47 +++++++++++++++++-- my-app/src/react-aria-starter/src/theme.css | 2 +- 6 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 my-app/src/icons/ChevronLeft.jsx create mode 100644 my-app/src/icons/ChevronRight.jsx diff --git a/my-app/src/icons/ChevronLeft.jsx b/my-app/src/icons/ChevronLeft.jsx new file mode 100644 index 0000000..f3f5eef --- /dev/null +++ b/my-app/src/icons/ChevronLeft.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function ChevronLeft({ className, color, disabled, ...props }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/my-app/src/icons/ChevronRight.jsx b/my-app/src/icons/ChevronRight.jsx new file mode 100644 index 0000000..dabe0ad --- /dev/null +++ b/my-app/src/icons/ChevronRight.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function ChevronRight({ className, color, disabled, ...props }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index 3e68b45..04885ca 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -17,7 +17,13 @@ } .bookingTimesContainer { - background-color: red; + margin-top: 2rem; + padding: 2rem; + border-radius: 0.3rem; + outline: 1px solid #E7E7E7; + display: flex; + flex-direction: column; + align-items: center; } diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index ef00219..6bbd5a3 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -13,16 +13,55 @@ display: flex; width: fit-content; align-items: center; + width: fit-content + } + + .react-aria-Group { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + + .chevron-button { + background: none; + border: none; + padding: 0.5rem; + margin: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s, opacity 0.2s; + } + + .chevron-button:hover:not(:disabled) { + background-color: rgba(0, 0, 0, 0.05); + } + + .chevron-button:active:not(:disabled) { + background-color: rgba(0, 0, 0, 0.1); + } + + .chevron-button:disabled { + cursor: not-allowed; + opacity: 0.3; + } + + .chevron-button:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; } .react-aria-Button { - background: var(--highlight-background); - color: var(--highlight-foreground); + /*background: var(--highlight-background);*/ + /*color: var(--highlight-foreground);*/ border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; - border: none; - margin-left: -1.929rem; + /*border: none;*/ + border: 1px solid #D4D4D4; /*width: 1.429rem;*/ /*height: 1.429rem;*/ width: fit-content; diff --git a/my-app/src/react-aria-starter/src/DatePicker.tsx b/my-app/src/react-aria-starter/src/DatePicker.tsx index 36e57ab..4947b22 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.tsx +++ b/my-app/src/react-aria-starter/src/DatePicker.tsx @@ -21,24 +21,40 @@ import { import './DatePicker.css'; import { convertDateObjectToString } from '../../helpers'; +import ChevronLeft from '../../icons/ChevronLeft'; +import ChevronRight from '../../icons/ChevronRight'; export interface DatePickerProps extends AriaDatePickerProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); + chevronColor?: string; + canNavigatePrevious?: boolean; + canNavigateNext?: boolean; + onPreviousClick?: () => void; + onNextClick?: () => void; } export function DatePicker( - { label, description, errorMessage, firstDayOfWeek, ...props }: - DatePickerProps + { + label, + description, + errorMessage, + firstDayOfWeek, + chevronColor = "#666", + canNavigatePrevious = true, + canNavigateNext = true, + onPreviousClick, + onNextClick, + ...props + }: DatePickerProps ) { return ( ( - { /* @@ -47,8 +63,29 @@ export function DatePicker( */ } - - + + + + + {description && {description}} {errorMessage} diff --git a/my-app/src/react-aria-starter/src/theme.css b/my-app/src/react-aria-starter/src/theme.css index 247f02c..c0c1cee 100644 --- a/my-app/src/react-aria-starter/src/theme.css +++ b/my-app/src/react-aria-starter/src/theme.css @@ -83,7 +83,7 @@ --button-background-pressed: var(--background-color); /* these colors are the same between light and dark themes * to ensure contrast with the foreground color */ - --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ + --highlight-background: #3e70ec; /* purple-300 from dark theme, 3.03:1 against background-color */ --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ --highlight-foreground: white; /* 5.56:1 against highlight-background */ -- 2.39.5 From 8863b2a910ee256e58a79cee153320f2a878fa58 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:21:17 +0200 Subject: [PATCH 05/41] Remove BookingFormFields --- my-app/src/components/BookingFormFields.jsx | 65 ------------------- my-app/src/components/BookingLengthField.jsx | 2 +- .../components/BookingLengthField.module.css | 10 +++ my-app/src/components/BookingTitleField.jsx | 2 +- ...odule.css => BookingTitleField.module.css} | 0 my-app/src/components/ParticipantsField.jsx | 2 +- .../components/ParticipantsField.module.css | 10 +++ my-app/src/components/RoomSelectionField.jsx | 2 +- .../components/RoomSelectionField.module.css | 10 +++ 9 files changed, 34 insertions(+), 69 deletions(-) delete mode 100644 my-app/src/components/BookingFormFields.jsx create mode 100644 my-app/src/components/BookingLengthField.module.css rename my-app/src/components/{BookingFormFields.module.css => BookingTitleField.module.css} (100%) create mode 100644 my-app/src/components/ParticipantsField.module.css create mode 100644 my-app/src/components/RoomSelectionField.module.css diff --git a/my-app/src/components/BookingFormFields.jsx b/my-app/src/components/BookingFormFields.jsx deleted file mode 100644 index 2b1eb48..0000000 --- a/my-app/src/components/BookingFormFields.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import Dropdown from './Dropdown'; -import { ComboBox } from './ComboBox'; -import { DEFAULT_BOOKING_TITLE, BOOKING_LENGTHS, SMALL_GROUP_ROOMS, PEOPLE } from '../constants/bookingConstants'; -import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; - -export function BookingFormFields() { - const booking = useBookingContext(); - return ( - <> -

Titel på bokning

- booking.setTitle(event.target.value)} - placeholder={DEFAULT_BOOKING_TITLE} - className={styles.textInput} - /> - -

Deltagare

- - - {booking.participants.length > 0 && ( -
- {booking.participants.map((participant, index) => ( -

{participant}

- ))} -
- )} - -
-
-

Rum

- booking.handleRoomChange(e)} - placeholder={{ - label: "Alla rum", - value: "allRooms" - }} - /> -
-
-

Längd

- booking.handleLengthChange(Number(e.target.value))} - placeholder={{ - label: "Alla tider", - value: 0 - }} - disabledOptions={booking.disabledOptions} - /> -
-
- - ); -} \ No newline at end of file diff --git a/my-app/src/components/BookingLengthField.jsx b/my-app/src/components/BookingLengthField.jsx index 0e86bb5..5af8eac 100644 --- a/my-app/src/components/BookingLengthField.jsx +++ b/my-app/src/components/BookingLengthField.jsx @@ -2,7 +2,7 @@ import React from 'react'; import Dropdown from './Dropdown'; import { BOOKING_LENGTHS } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; +import styles from './BookingLengthField.module.css'; export function BookingLengthField() { const booking = useBookingContext(); diff --git a/my-app/src/components/BookingLengthField.module.css b/my-app/src/components/BookingLengthField.module.css new file mode 100644 index 0000000..f804eca --- /dev/null +++ b/my-app/src/components/BookingLengthField.module.css @@ -0,0 +1,10 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/BookingTitleField.jsx b/my-app/src/components/BookingTitleField.jsx index 6317ec4..343cc4e 100644 --- a/my-app/src/components/BookingTitleField.jsx +++ b/my-app/src/components/BookingTitleField.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; +import styles from './BookingTitleField.module.css'; export function BookingTitleField() { const booking = useBookingContext(); diff --git a/my-app/src/components/BookingFormFields.module.css b/my-app/src/components/BookingTitleField.module.css similarity index 100% rename from my-app/src/components/BookingFormFields.module.css rename to my-app/src/components/BookingTitleField.module.css diff --git a/my-app/src/components/ParticipantsField.jsx b/my-app/src/components/ParticipantsField.jsx index 57e1c83..4cebd27 100644 --- a/my-app/src/components/ParticipantsField.jsx +++ b/my-app/src/components/ParticipantsField.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { ComboBox } from './ComboBox'; import { PEOPLE } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; +import styles from './ParticipantsField.module.css'; export function ParticipantsField() { const booking = useBookingContext(); diff --git a/my-app/src/components/ParticipantsField.module.css b/my-app/src/components/ParticipantsField.module.css new file mode 100644 index 0000000..f804eca --- /dev/null +++ b/my-app/src/components/ParticipantsField.module.css @@ -0,0 +1,10 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/RoomSelectionField.jsx b/my-app/src/components/RoomSelectionField.jsx index 37c70d3..df0d13e 100644 --- a/my-app/src/components/RoomSelectionField.jsx +++ b/my-app/src/components/RoomSelectionField.jsx @@ -2,7 +2,7 @@ import React from 'react'; import Dropdown from './Dropdown'; import { SMALL_GROUP_ROOMS } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; -import styles from './BookingFormFields.module.css'; +import styles from './RoomSelectionField.module.css'; export function RoomSelectionField() { const booking = useBookingContext(); diff --git a/my-app/src/components/RoomSelectionField.module.css b/my-app/src/components/RoomSelectionField.module.css new file mode 100644 index 0000000..f804eca --- /dev/null +++ b/my-app/src/components/RoomSelectionField.module.css @@ -0,0 +1,10 @@ +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} \ No newline at end of file -- 2.39.5 From dc028da9f8347f466142035c1de46c49b9e06c67 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:40:52 +0200 Subject: [PATCH 06/41] small fields fixes --- my-app/src/components/BookingDatePicker.jsx | 18 ++++++++---------- my-app/src/components/ComboBox.module.css | 2 +- .../src/react-aria-starter/src/DatePicker.css | 13 ++++++------- my-app/src/react-aria-starter/src/theme.css | 6 +++--- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index 57d3579..a0bf325 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -8,16 +8,14 @@ export function BookingDatePicker() { const booking = useBookingContext(); return (
-
- booking.handleDateChange(date)} - firstDayOfWeek="mon" - minValue={today(getLocalTimeZone())} - maxValue={getFutureDate(14)} - isDateUnavailable={isDateUnavailable} - /> -
+ booking.handleDateChange(date)} + firstDayOfWeek="mon" + minValue={today(getLocalTimeZone())} + maxValue={getFutureDate(14)} + isDateUnavailable={isDateUnavailable} + />
); } \ No newline at end of file diff --git a/my-app/src/components/ComboBox.module.css b/my-app/src/components/ComboBox.module.css index 7aceaac..7dd6445 100644 --- a/my-app/src/components/ComboBox.module.css +++ b/my-app/src/components/ComboBox.module.css @@ -15,7 +15,7 @@ color: var(--field-text-color); border: 1px solid var(--border-color); border-radius: 6px; - padding: 0.286rem 2rem 0.286rem 0.571rem; + padding: 1rem; vertical-align: middle; outline: none; min-width: 0; diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index 6bbd5a3..de534c7 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -13,14 +13,12 @@ display: flex; width: fit-content; align-items: center; - width: fit-content + width: fit-content; + gap: 2rem; } - .react-aria-Group { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; + .react-aria-Button { + min-width: fit-content; } .chevron-button { @@ -71,7 +69,8 @@ &[data-pressed] { box-shadow: none; - background: var(--highlight-background); + /*background: var(--highlight-background);*/ + background: #f1f1f1; } &[data-focus-visible] { diff --git a/my-app/src/react-aria-starter/src/theme.css b/my-app/src/react-aria-starter/src/theme.css index c0c1cee..b4bc1e3 100644 --- a/my-app/src/react-aria-starter/src/theme.css +++ b/my-app/src/react-aria-starter/src/theme.css @@ -10,10 +10,10 @@ * Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ :root { --background-color: #f8f8f8; - --gray-50: #ffffff; + --gray-50: #FAFBFC; --gray-100: #d0d0d0; --gray-200: #afafaf; - --gray-300: #8f8f8f; + --gray-300: #CECECE; --gray-400: #717171; --gray-500: #555555; --gray-600: #393939; @@ -40,7 +40,7 @@ --gray-50: #101010; --gray-100: #393939; --gray-200: #4f4f4f; - --gray-300: #686868; + --gray-300: #CECECE; --gray-400: #848484; --gray-500: #a7a7a7; --gray-600: #cfcfcf; -- 2.39.5 From 090bb5c830222697ab7f399989eb2dc7c4c61cdc Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:17:07 +0200 Subject: [PATCH 07/41] participants improved! but not finished --- .../src/components/ParticipantsSelector.jsx | 118 ++++++++++++ .../ParticipantsSelector.module.css | 182 ++++++++++++++++++ my-app/src/constants/bookingConstants.js | 12 +- my-app/src/hooks/useBookingState.js | 23 ++- my-app/src/pages/NewBooking.jsx | 4 +- 5 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 my-app/src/components/ParticipantsSelector.jsx create mode 100644 my-app/src/components/ParticipantsSelector.module.css diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx new file mode 100644 index 0000000..e60f460 --- /dev/null +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -0,0 +1,118 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { PEOPLE } from '../constants/bookingConstants'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './ParticipantsSelector.module.css'; + +export function ParticipantsSelector() { + const booking = useBookingContext(); + const [searchTerm, setSearchTerm] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [recentSearches, setRecentSearches] = useState([]); + const inputRef = useRef(null); + const dropdownRef = useRef(null); + + // Filter people based on search term + const filteredPeople = PEOPLE.filter(person => + person.name.toLowerCase().includes(searchTerm.toLowerCase()) || + person.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Show all people when empty search, filtered when typing + const displayPeople = searchTerm === '' ? PEOPLE : filteredPeople; + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleInputFocus = () => { + setIsDropdownOpen(true); + }; + + const handleInputChange = (e) => { + setSearchTerm(e.target.value); + setIsDropdownOpen(true); + }; + + const handleSelectPerson = (person) => { + console.log('handleSelectPerson called with:', person); + booking.handleParticipantChange(person.id); + setSearchTerm(''); + setIsDropdownOpen(false); + inputRef.current?.blur(); + }; + + const handleRemoveParticipant = (participantToRemove) => { + booking.handleRemoveParticipant(participantToRemove); + }; + + return ( +
+

Deltagare

+ + {/* Search Input */} +
+ + + {/* Dropdown */} + {isDropdownOpen && ( +
+ {displayPeople.length > 0 ? ( +
+ {displayPeople.map((person) => ( +
handleSelectPerson(person)} + > +
+
{person.name}
+
{person.email}
+
+
+ ))} +
+ ) : ( +
+ No participants found +
+ )} +
+ )} +
+ + {/* Selected Participants */} + {booking.participants.length > 0 && ( +
+ {booking.participants.map((participant, index) => ( +
+ {participant} + +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css new file mode 100644 index 0000000..5c7c86c --- /dev/null +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -0,0 +1,182 @@ +.container { + position: relative; +} + +.elementHeading { + margin: 0; + color: #8E8E8E; + font-size: 0.8rem; + font-style: normal; + font-weight: 520; + line-height: normal; + margin-bottom: 0.2rem; + margin-top: 1.5rem; +} + +.selectedParticipants { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; + padding: 0; +} + +.participantChip { + display: flex; + align-items: center; + background-color: #F0F8FF; + border: 1px solid #D1E7FF; + border-radius: 1.25rem; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + color: #2563EB; + gap: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.participantChip:hover { + background-color: #E0F2FE; + border-color: #BAE6FD; +} + +.participantName { + font-weight: 500; +} + +.removeButton { + background: rgba(37, 99, 235, 0.1); + border: none; + color: #2563EB; + cursor: pointer; + font-size: 0.875rem; + line-height: 1; + padding: 0; + width: 1.25rem; + height: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; +} + +.removeButton:hover { + background-color: rgba(37, 99, 235, 0.2); + transform: scale(1.1); +} + +.searchContainer { + position: relative; + width: 100%; + max-width: 600px; +} + +.searchInput { + width: 100%; + margin-bottom: 10px; + border: 1px solid #D2D9E0; + border-radius: 0.5rem; + font-size: 16px; + background-color: #FAFBFC; + padding: 1rem; + font-family: inherit; + box-sizing: border-box; +} + +.searchInput::placeholder { + color: #adadad; +} + +.searchInput:focus { + outline: 2px solid var(--focus-ring-color, #3e70ec); + outline-offset: -1px; +} + +.dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #D2D9E0; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); + z-index: 1000; + max-height: 300px; + overflow-y: auto; + margin-top: -10px; +} + +.section { + padding: 0.5rem 0; +} + +.section:not(:last-child) { + border-bottom: 1px solid #F1F3F4; +} + +.sectionHeader { + font-weight: 600; + color: #5F6368; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0.25rem 1rem; + margin-bottom: 0.25rem; +} + +.dropdownItem { + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.2s; + border: none; + background: none; + width: 100%; + text-align: left; + font-family: inherit; +} + +.dropdownItem:hover { + background-color: #F8F9FA; +} + +.dropdownItem:active { + background-color: #E8F0FE; +} + +.personInfo { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.personName { + font-weight: 500; + color: #202124; + font-size: 0.875rem; +} + +.personEmail { + font-size: 0.75rem; + color: #5F6368; +} + +.addNewItem { + color: #1A73E8; + font-weight: 500; + opacity: 0.5; + cursor: not-allowed; +} + +.addNewItem:hover { + background-color: transparent; +} + +.noResults { + padding: 1rem; + text-align: center; + color: #5F6368; + font-size: 0.875rem; + font-style: italic; +} \ No newline at end of file diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index af4eba1..6323e81 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -19,12 +19,12 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({ })); export const PEOPLE = [ - { id: 1, name: 'Arjohn Emilsson' }, - { id: 2, name: 'Filip Norgren' }, - { id: 3, name: 'Hedvig Engelmark' }, - { id: 4, name: 'Elin Rudling' }, - { id: 5, name: 'Victor Magnusson' }, - { id: 6, name: 'Ellen Britschgi' } + { id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 2, name: 'Filip Norgren', email: 'filip.norgren@dsv.su.se' }, + { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, + { id: 4, name: 'Elin Rudling', email: 'elin.rudling@dsv.su.se' }, + { id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' }, + { id: 6, name: 'Ellen Britschgi', email: 'ellen.britschgi@dsv.su.se' } ]; export const DEFAULT_DISABLED_OPTIONS = { diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index d0feb89..9bc39a1 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -6,7 +6,7 @@ import { generateId, findObjectById } from '../utils/bookingUtils'; -import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants'; +import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; export function useBookingState(addBooking) { @@ -98,11 +98,24 @@ export function useBookingState(addBooking) { } }, [selectedEndIndex]); - const handleParticipantChange = useCallback((participant) => { - if (participant !== null) { - setParticipants(prev => [...prev, participant.trim()]); + const handleParticipantChange = useCallback((participantId) => { + console.log('handleParticipantChange called with:', participantId); + if (participantId !== null && participantId !== undefined) { + // Find the person by ID and add their name + const person = PEOPLE.find(p => p.id === participantId); + console.log('Found person:', person); + if (person && !participants.includes(person.name)) { + console.log('Adding participant:', person.name); + setParticipants(prev => [...prev, person.name]); + } else { + console.log('Participant already exists or person not found'); + } setParticipant(""); } + }, [participants]); + + const handleRemoveParticipant = useCallback((participantToRemove) => { + setParticipants(prev => prev.filter(p => p !== participantToRemove)); }, []); // Memoize the return object to prevent unnecessary re-renders @@ -134,6 +147,7 @@ export function useBookingState(addBooking) { handleSave, handleTimeCardExit, handleParticipantChange, + handleRemoveParticipant, }), [ timeSlotsByRoom, currentRoom, @@ -155,5 +169,6 @@ export function useBookingState(addBooking) { handleSave, handleTimeCardExit, handleParticipantChange, + handleRemoveParticipant, ]); } \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index dff3060..f226bad 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -3,7 +3,7 @@ import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/TimeCardContainer'; import { BookingDatePicker } from '../components/BookingDatePicker'; import { BookingTitleField } from '../components/BookingTitleField'; -import { ParticipantsField } from '../components/ParticipantsField'; +import { ParticipantsSelector } from '../components/ParticipantsSelector'; import { RoomSelectionField } from '../components/RoomSelectionField'; import { BookingLengthField } from '../components/BookingLengthField'; import { useBookingState } from '../hooks/useBookingState'; @@ -19,7 +19,7 @@ export function NewBooking({ addBooking }) {
- +
-- 2.39.5 From ad4f8f6f0bc6b51199d4985f9af79fc22a4ac321 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:23:49 +0200 Subject: [PATCH 08/41] more improvements! --- .../src/components/ParticipantsSelector.jsx | 78 ++++++++++++++++--- .../ParticipantsSelector.module.css | 18 +++++ my-app/src/constants/bookingConstants.js | 11 ++- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index e60f460..781f485 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -7,7 +7,11 @@ export function ParticipantsSelector() { const booking = useBookingContext(); const [searchTerm, setSearchTerm] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [recentSearches, setRecentSearches] = useState([]); + const [recentSearches, setRecentSearches] = useState([ + { id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, + { id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' } + ]); const inputRef = useRef(null); const dropdownRef = useRef(null); @@ -17,8 +21,14 @@ export function ParticipantsSelector() { person.email.toLowerCase().includes(searchTerm.toLowerCase()) ); - // Show all people when empty search, filtered when typing - const displayPeople = searchTerm === '' ? PEOPLE : filteredPeople; + // Show recent searches only when input is empty (first click) + const showRecentSearches = searchTerm === '' && recentSearches.length > 0; + // Only show all people when user starts typing + const showAllPeople = searchTerm !== ''; + const displayPeople = filteredPeople; + + // Helper function to check if person is already selected + const isPersonSelected = (personName) => booking.participants.includes(personName); useEffect(() => { const handleClickOutside = (event) => { @@ -42,7 +52,22 @@ export function ParticipantsSelector() { const handleSelectPerson = (person) => { console.log('handleSelectPerson called with:', person); + + // Don't add if already selected + if (isPersonSelected(person.name)) { + setSearchTerm(''); + setIsDropdownOpen(false); + inputRef.current?.blur(); + return; + } + booking.handleParticipantChange(person.id); + + // Add to recent searches if not already there + if (!recentSearches.find(p => p.id === person.id)) { + setRecentSearches(prev => [person, ...prev.slice(0, 4)]); + } + setSearchTerm(''); setIsDropdownOpen(false); inputRef.current?.blur(); @@ -71,25 +96,54 @@ export function ParticipantsSelector() { {/* Dropdown */} {isDropdownOpen && (
- {displayPeople.length > 0 ? ( + {/* Recent Searches */} + {showRecentSearches && (
- {displayPeople.map((person) => ( +
Senaste sökningar
+ {recentSearches.map((person) => (
handleSelectPerson(person)} >
-
{person.name}
+
+ {person.name} + {isPersonSelected(person.name) && } +
{person.email}
))}
- ) : ( -
- No participants found -
+ )} + + {/* Search Results - Only when typing */} + {showAllPeople && ( + displayPeople.length > 0 ? ( +
+
Sökresultat
+ {displayPeople.map((person) => ( +
handleSelectPerson(person)} + > +
+
+ {person.name} + {isPersonSelected(person.name) && } +
+
{person.email}
+
+
+ ))} +
+ ) : ( +
+ Inga deltagare hittades +
+ ) )}
)} diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css index 5c7c86c..8cc3951 100644 --- a/my-app/src/components/ParticipantsSelector.module.css +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -155,6 +155,9 @@ font-weight: 500; color: #202124; font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: space-between; } .personEmail { @@ -173,6 +176,21 @@ background-color: transparent; } +.selectedItem { + background-color: #F0F8FF; + opacity: 0.7; +} + +.selectedItem:hover { + background-color: #E0F2FE; +} + +.selectedIndicator { + color: #2563EB; + font-weight: bold; + margin-left: 0.5rem; +} + .noResults { padding: 1rem; text-align: center; diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index 6323e81..feb4917 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -24,7 +24,16 @@ export const PEOPLE = [ { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, { id: 4, name: 'Elin Rudling', email: 'elin.rudling@dsv.su.se' }, { id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' }, - { id: 6, name: 'Ellen Britschgi', email: 'ellen.britschgi@dsv.su.se' } + { id: 6, name: 'Ellen Britschgi', email: 'ellen.britschgi@dsv.su.se' }, + { id: 7, name: 'Anna Andersson', email: 'anna.andersson@dsv.su.se' }, + { id: 8, name: 'Erik Larsson', email: 'erik.larsson@dsv.su.se' }, + { id: 9, name: 'Sofia Karlsson', email: 'sofia.karlsson@dsv.su.se' }, + { id: 10, name: 'Magnus Nilsson', email: 'magnus.nilsson@dsv.su.se' }, + { id: 11, name: 'Emma Johansson', email: 'emma.johansson@dsv.su.se' }, + { id: 12, name: 'Oskar Pettersson', email: 'oskar.pettersson@dsv.su.se' }, + { id: 13, name: 'Linda Svensson', email: 'linda.svensson@dsv.su.se' }, + { id: 14, name: 'Jonas Gustafsson', email: 'jonas.gustafsson@dsv.su.se' }, + { id: 15, name: 'Maria Olsson', email: 'maria.olsson@dsv.su.se' } ]; export const DEFAULT_DISABLED_OPTIONS = { -- 2.39.5 From e604bb21b1f902a0dc9e59fc0e3577439232d5fc Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:36:07 +0200 Subject: [PATCH 09/41] =?UTF-8?q?vibe=20code=20=C3=A4r=20kul=20idag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParticipantsSelector.jsx | 91 +++++++++++++++++-- .../ParticipantsSelector.module.css | 25 +++++ 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index 781f485..9dcf7e8 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -7,6 +7,7 @@ export function ParticipantsSelector() { const booking = useBookingContext(); const [searchTerm, setSearchTerm] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); const [recentSearches, setRecentSearches] = useState([ { id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' }, { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, @@ -29,6 +30,9 @@ export function ParticipantsSelector() { // Helper function to check if person is already selected const isPersonSelected = (personName) => booking.participants.includes(personName); + + // Get all available options for keyboard navigation + const allOptions = showRecentSearches ? recentSearches : (showAllPeople ? displayPeople : []); useEffect(() => { const handleClickOutside = (event) => { @@ -42,12 +46,60 @@ export function ParticipantsSelector() { }, []); const handleInputFocus = () => { + // Don't auto-open dropdown on focus - wait for user interaction + setFocusedIndex(-1); + }; + + const handleInputClick = () => { setIsDropdownOpen(true); + setFocusedIndex(-1); }; const handleInputChange = (e) => { setSearchTerm(e.target.value); setIsDropdownOpen(true); + setFocusedIndex(-1); + }; + + const handleKeyDown = (e) => { + if (!isDropdownOpen) { + // When dropdown is closed, Enter should open it + if (e.key === 'Enter') { + e.preventDefault(); + setIsDropdownOpen(true); + setFocusedIndex(-1); + } + return; + } + + switch (e.key) { + case 'ArrowDown': + case 'Tab': + e.preventDefault(); + setFocusedIndex(prev => + prev < allOptions.length - 1 ? prev + 1 : 0 + ); + break; + case 'ArrowUp': + e.preventDefault(); + setFocusedIndex(prev => + prev > 0 ? prev - 1 : allOptions.length - 1 + ); + break; + case 'Enter': + e.preventDefault(); + if (focusedIndex >= 0 && focusedIndex < allOptions.length) { + handleSelectPerson(allOptions[focusedIndex]); + } + break; + case 'Escape': + e.preventDefault(); + setIsDropdownOpen(false); + setFocusedIndex(-1); + // Keep input focused with outline, don't blur completely + inputRef.current?.focus(); + break; + } }; const handleSelectPerson = (person) => { @@ -57,7 +109,9 @@ export function ParticipantsSelector() { if (isPersonSelected(person.name)) { setSearchTerm(''); setIsDropdownOpen(false); - inputRef.current?.blur(); + setFocusedIndex(-1); + // Keep focus on input instead of blurring + inputRef.current?.focus(); return; } @@ -70,7 +124,9 @@ export function ParticipantsSelector() { setSearchTerm(''); setIsDropdownOpen(false); - inputRef.current?.blur(); + setFocusedIndex(-1); + // Keep focus on input for continued interaction + inputRef.current?.focus(); }; const handleRemoveParticipant = (participantToRemove) => { @@ -89,22 +145,35 @@ export function ParticipantsSelector() { value={searchTerm} onChange={handleInputChange} onFocus={handleInputFocus} + onClick={handleInputClick} + onKeyDown={handleKeyDown} placeholder="Search for participants..." className={styles.searchInput} + role="combobox" + aria-expanded={isDropdownOpen} + aria-autocomplete="list" + aria-activedescendant={focusedIndex >= 0 ? `option-${focusedIndex}` : undefined} /> {/* Dropdown */} {isDropdownOpen && ( -
+
{/* Recent Searches */} {showRecentSearches && (
Senaste sökningar
- {recentSearches.map((person) => ( + {recentSearches.map((person, index) => (
handleSelectPerson(person)} + role="option" + aria-selected={isPersonSelected(person.name)} >
@@ -123,11 +192,14 @@ export function ParticipantsSelector() { displayPeople.length > 0 ? (
Sökresultat
- {displayPeople.map((person) => ( + {displayPeople.map((person, index) => (
handleSelectPerson(person)} + role="option" + aria-selected={isPersonSelected(person.name)} >
@@ -158,8 +230,11 @@ export function ParticipantsSelector() { diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css index 8cc3951..93fa840 100644 --- a/my-app/src/components/ParticipantsSelector.module.css +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -66,6 +66,17 @@ transform: scale(1.1); } +.removeButton:focus { + outline: 2px solid var(--focus-ring-color, #3B82F6); + outline-offset: 2px; + background-color: rgba(37, 99, 235, 0.3); +} + +.chipFocused { + box-shadow: 0 0 0 2px var(--focus-ring-color, #3B82F6) !important; + border-color: var(--focus-ring-color, #3B82F6) !important; +} + .searchContainer { position: relative; width: 100%; @@ -91,6 +102,7 @@ .searchInput:focus { outline: 2px solid var(--focus-ring-color, #3e70ec); outline-offset: -1px; + border-color: var(--focus-ring-color, #3e70ec); } .dropdown { @@ -191,6 +203,19 @@ margin-left: 0.5rem; } +.focusedItem { + background-color: #3B82F6 !important; + color: white !important; +} + +.focusedItem .personEmail { + color: rgba(255, 255, 255, 0.8) !important; +} + +.focusedItem .selectedIndicator { + color: white !important; +} + .noResults { padding: 1rem; text-align: center; -- 2.39.5 From eef9cf1e799ba56c75a9f7d2cb02ff6e738dbe1b Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:55:16 +0200 Subject: [PATCH 10/41] color fix --- my-app/src/components/ParticipantsSelector.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css index 93fa840..e9ac960 100644 --- a/my-app/src/components/ParticipantsSelector.module.css +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -208,6 +208,10 @@ color: white !important; } +.focusedItem .personName { + color: white !important; +} + .focusedItem .personEmail { color: rgba(255, 255, 255, 0.8) !important; } -- 2.39.5 From 725d4d98775b07171dcd034c160180513abddf74 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:37:09 +0200 Subject: [PATCH 11/41] =?UTF-8?q?Typ=20klar=20f=C3=B6r=20nu=20med=20partic?= =?UTF-8?q?ipants=3F=3F=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParticipantsSelector.jsx | 79 ++++++---- .../ParticipantsSelector.module.css | 113 ++++++++++----- my-app/src/constants/bookingConstants.js | 137 ++++++++++++++++-- my-app/src/hooks/useBookingState.js | 8 +- 4 files changed, 258 insertions(+), 79 deletions(-) diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index 9dcf7e8..d32fafd 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { PEOPLE } from '../constants/bookingConstants'; +import { PEOPLE, USER } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; import styles from './ParticipantsSelector.module.css'; @@ -9,9 +9,9 @@ export function ParticipantsSelector() { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const [recentSearches, setRecentSearches] = useState([ - { id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' }, - { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, - { id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' } + { id: 1, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, + { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' } ]); const inputRef = useRef(null); const dropdownRef = useRef(null); @@ -19,6 +19,7 @@ export function ParticipantsSelector() { // Filter people based on search term const filteredPeople = PEOPLE.filter(person => person.name.toLowerCase().includes(searchTerm.toLowerCase()) || + person.username.toLowerCase().includes(searchTerm.toLowerCase()) || person.email.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -29,10 +30,15 @@ export function ParticipantsSelector() { const displayPeople = filteredPeople; // Helper function to check if person is already selected - const isPersonSelected = (personName) => booking.participants.includes(personName); + const isPersonSelected = (personName) => booking.participants.find(p => p.name === personName); // Get all available options for keyboard navigation const allOptions = showRecentSearches ? recentSearches : (showAllPeople ? displayPeople : []); + + // Helper function to get person's initials + const getInitials = (name) => { + return name.split(' ').map(word => word[0]).join('').toUpperCase(); + }; useEffect(() => { const handleClickOutside = (event) => { @@ -175,12 +181,21 @@ export function ParticipantsSelector() { role="option" aria-selected={isPersonSelected(person.name)} > +
+ {person.profilePicture ? ( + {person.name} + ) : ( +
+ {getInitials(person.name)} +
+ )} +
{person.name} {isPersonSelected(person.name) && }
-
{person.email}
+
{person.username}
))} @@ -201,12 +216,21 @@ export function ParticipantsSelector() { role="option" aria-selected={isPersonSelected(person.name)} > +
+ {person.profilePicture ? ( + {person.name} + ) : ( +
+ {getInitials(person.name)} +
+ )} +
{person.name} {isPersonSelected(person.name) && }
-
{person.email}
+
{person.username}
))} @@ -222,26 +246,27 @@ export function ParticipantsSelector() {
{/* Selected Participants */} - {booking.participants.length > 0 && ( -
- {booking.participants.map((participant, index) => ( -
- {participant} - -
- ))} -
- )} +
+ {/* Default User (Non-deletable) */} +
+ {USER.name} +
+ + {/* Additional Participants (Deletable) */} + {booking.participants.map((participant, index) => ( + + ))} +
); } \ No newline at end of file diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css index e9ac960..cbbe0d4 100644 --- a/my-app/src/components/ParticipantsSelector.module.css +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -17,7 +17,6 @@ display: flex; flex-wrap: wrap; gap: 0.5rem; - margin-top: 0.75rem; padding: 0; } @@ -35,48 +34,55 @@ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } -.participantChip:hover { - background-color: #E0F2FE; - border-color: #BAE6FD; -} .participantName { font-weight: 500; } -.removeButton { - background: rgba(37, 99, 235, 0.1); - border: none; - color: #2563EB; +.clickableChip { cursor: pointer; - font-size: 0.875rem; - line-height: 1; - padding: 0; - width: 1.25rem; - height: 1.25rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; transition: all 0.2s ease; + width: fit-content; + min-width: auto; } -.removeButton:hover { - background-color: rgba(37, 99, 235, 0.2); - transform: scale(1.1); +.clickableChip:hover { + background-color: #E0F2FE; + border-color: #BAE6FD; } -.removeButton:focus { - outline: 2px solid var(--focus-ring-color, #3B82F6); +.clickableChip:focus { + outline: 2px solid #2563EB; outline-offset: 2px; - background-color: rgba(37, 99, 235, 0.3); } -.chipFocused { - box-shadow: 0 0 0 2px var(--focus-ring-color, #3B82F6) !important; - border-color: var(--focus-ring-color, #3B82F6) !important; +.clickableChip:active { + background-color: #BFDBFE; + transform: scale(0.98); } + +.removeIcon { + color: #2563EB; + font-size: 0.875rem; + font-weight: bold; + margin-left: 0.25rem; +} + +.defaultUserChip { + background-color: #F3F4F6; + border-color: #D1D5DB; + color: #374151; +} + +.defaultUserChip:hover { + background-color: #F3F4F6; + border-color: #D1D5DB; + color: #374151; + cursor: default; +} + + .searchContainer { position: relative; width: 100%; @@ -100,9 +106,9 @@ } .searchInput:focus { - outline: 2px solid var(--focus-ring-color, #3e70ec); + outline: 2px solid #2563EB; outline-offset: -1px; - border-color: var(--focus-ring-color, #3e70ec); + border-color: #2563EB; } .dropdown { @@ -117,7 +123,7 @@ z-index: 1000; max-height: 300px; overflow-y: auto; - margin-top: -10px; + margin-top: 0rem; } .section { @@ -143,10 +149,18 @@ cursor: pointer; transition: background-color 0.2s; border: none; + border-bottom: 1px solid #F1F3F4; background: none; width: 100%; text-align: left; font-family: inherit; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.dropdownItem:last-child { + border-bottom: none; } .dropdownItem:hover { @@ -157,10 +171,38 @@ background-color: #E8F0FE; } +.personAvatar { + width: 2rem; + height: 2rem; + border-radius: 50%; + flex-shrink: 0; +} + +.avatarImage { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.avatarInitials { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #2563EB; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; +} + .personInfo { display: flex; flex-direction: column; gap: 0.125rem; + flex: 1; } .personName { @@ -172,7 +214,7 @@ justify-content: space-between; } -.personEmail { +.personUsername { font-size: 0.75rem; color: #5F6368; } @@ -204,7 +246,7 @@ } .focusedItem { - background-color: #3B82F6 !important; + background-color: #2563EB !important; color: white !important; } @@ -212,7 +254,7 @@ color: white !important; } -.focusedItem .personEmail { +.focusedItem .personUsername { color: rgba(255, 255, 255, 0.8) !important; } @@ -220,6 +262,11 @@ color: white !important; } +.focusedItem .avatarInitials { + background-color: rgba(255, 255, 255, 0.2); + color: white; +} + .noResults { padding: 1rem; text-align: center; diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index feb4917..19c6b2b 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -19,23 +19,130 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({ })); export const PEOPLE = [ - { id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' }, - { id: 2, name: 'Filip Norgren', email: 'filip.norgren@dsv.su.se' }, - { id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' }, - { id: 4, name: 'Elin Rudling', email: 'elin.rudling@dsv.su.se' }, - { id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' }, - { id: 6, name: 'Ellen Britschgi', email: 'ellen.britschgi@dsv.su.se' }, - { id: 7, name: 'Anna Andersson', email: 'anna.andersson@dsv.su.se' }, - { id: 8, name: 'Erik Larsson', email: 'erik.larsson@dsv.su.se' }, - { id: 9, name: 'Sofia Karlsson', email: 'sofia.karlsson@dsv.su.se' }, - { id: 10, name: 'Magnus Nilsson', email: 'magnus.nilsson@dsv.su.se' }, - { id: 11, name: 'Emma Johansson', email: 'emma.johansson@dsv.su.se' }, - { id: 12, name: 'Oskar Pettersson', email: 'oskar.pettersson@dsv.su.se' }, - { id: 13, name: 'Linda Svensson', email: 'linda.svensson@dsv.su.se' }, - { id: 14, name: 'Jonas Gustafsson', email: 'jonas.gustafsson@dsv.su.se' }, - { id: 15, name: 'Maria Olsson', email: 'maria.olsson@dsv.su.se' } + { id: 1, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@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' }, + { 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' }, + { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' }, + { id: 9, name: 'Sofia Karlsson', username: 'soka1245', email: 'sofia.karlsson@dsv.su.se' }, + { id: 10, name: 'Magnus Nilsson', username: 'mani6789', email: 'magnus.nilsson@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: 13, name: 'Linda Svensson', username: 'lisv2174', email: 'linda.svensson@dsv.su.se' }, + { id: 14, name: 'Jonas Gustafsson', username: 'jogu8523', email: 'jonas.gustafsson@dsv.su.se' }, + { id: 15, name: 'Maria Olsson', username: 'maol7456', email: 'maria.olsson@dsv.su.se' }, + { id: 16, name: 'Andreas Berg', username: 'anbe9832', email: 'andreas.berg@dsv.su.se' }, + { id: 17, name: 'Lina Dahlberg', username: 'lida2165', email: 'lina.dahlberg@dsv.su.se' }, + { id: 18, name: 'Marcus Forsberg', username: 'mafo6754', email: 'marcus.forsberg@dsv.su.se' }, + { id: 19, name: 'Julia Hedström', username: 'juhe4921', email: 'julia.hedström@dsv.su.se' }, + { id: 20, name: 'Daniel Lindqvist', username: 'dali8374', email: 'daniel.lindqvist@dsv.su.se' }, + { id: 21, name: 'Sara Blomqvist', username: 'sabl1598', email: 'sara.blomqvist@dsv.su.se' }, + { id: 22, name: 'Henrik Lundberg', username: 'helu7263', email: 'henrik.lundberg@dsv.su.se' }, + { id: 23, name: 'Frida Engström', username: 'fren3847', email: 'frida.engström@dsv.su.se' }, + { id: 24, name: 'Tobias Sjöberg', username: 'tosj5691', email: 'tobias.sjöberg@dsv.su.se' }, + { id: 25, name: 'Amanda Wallin', username: 'amwa9254', email: 'amanda.wallin@dsv.su.se' }, + { id: 26, name: 'Mattias Holm', username: 'maho8173', email: 'mattias.holm@dsv.su.se' }, + { id: 27, name: 'Emelie Sandberg', username: 'emsa4629', email: 'emelie.sandberg@dsv.su.se' }, + { id: 28, name: 'Robin Åberg', username: 'roab7485', email: 'robin.åberg@dsv.su.se' }, + { id: 29, name: 'Caroline Ekström', username: 'caek2916', email: 'caroline.ekström@dsv.su.se' }, + { id: 30, name: 'Alexander Nyström', username: 'alny6387', email: 'alexander.nyström@dsv.su.se' }, + { id: 31, name: 'Lisa Berggren', username: 'libe5142', email: 'lisa.berggren@dsv.su.se' }, + { id: 32, name: 'David Holmberg', username: 'daho8764', email: 'david.holmberg@dsv.su.se' }, + { id: 33, name: 'Maja Lindström', username: 'mali3571', email: 'maja.lindström@dsv.su.se' }, + { id: 34, name: 'Johan Carlsson', username: 'joca6928', email: 'johan.carlsson@dsv.su.se' }, + { id: 35, name: 'Rebecka Svensson', username: 'resv4816', email: 'rebecka.svensson@dsv.su.se' }, + { id: 36, name: 'Niklas Hedberg', username: 'nihe7395', email: 'niklas.hedberg@dsv.su.se' }, + { id: 37, name: 'Ida Persson', username: 'idpe2583', email: 'ida.persson@dsv.su.se' }, + { id: 38, name: 'Martin Öberg', username: 'maöb5947', email: 'martin.öberg@dsv.su.se' }, + { id: 39, name: 'Therese Löfgren', username: 'thlo8162', email: 'therese.löfgren@dsv.su.se' }, + { id: 40, name: 'Stefan Martinsson', username: 'stma4739', email: 'stefan.martinsson@dsv.su.se' }, + { id: 41, name: 'Johanna Stenberg', username: 'jost6254', email: 'johanna.stenberg@dsv.su.se' }, + { id: 42, name: 'Peter Wikström', username: 'pewi9481', email: 'peter.wikström@dsv.su.se' }, + { id: 43, name: 'Mikaela Fransson', username: 'mifr3825', email: 'mikaela.fransson@dsv.su.se' }, + { id: 44, name: 'Christian Lindgren', username: 'chli7694', email: 'christian.lindgren@dsv.su.se' }, + { id: 45, name: 'Evelina Norberg', username: 'evno2147', email: 'evelina.norberg@dsv.su.se' }, + { id: 46, name: 'Andreas Strömberg', username: 'anst5863', email: 'andreas.strömberg@dsv.su.se' }, + { id: 47, name: 'Klara Danielsson', username: 'klda8429', email: 'klara.danielsson@dsv.su.se' }, + { id: 48, name: 'Simon Eklund', username: 'siek6175', email: 'simon.eklund@dsv.su.se' }, + { id: 49, name: 'Elsa Lundgren', username: 'ellu4892', email: 'elsa.lundgren@dsv.su.se' }, + { id: 50, name: 'Mikael Höglund', username: 'mihö7316', email: 'mikael.höglund@dsv.su.se' }, + { id: 51, name: 'Jennie Söderberg', username: 'jesö3754', email: 'jennie.söderberg@dsv.su.se' }, + { id: 52, name: 'Fredrik Åström', username: 'frås9128', email: 'fredrik.åström@dsv.su.se' }, + { id: 53, name: 'Cornelia Sundberg', username: 'cosu5683', email: 'cornelia.sundberg@dsv.su.se' }, + { id: 54, name: 'Emil Palmberg', username: 'empa8297', email: 'emil.palmberg@dsv.su.se' }, + { id: 55, name: 'Josefin Ringström', username: 'jori4612', email: 'josefin.ringström@dsv.su.se' }, + { id: 56, name: 'Christofer Åkesson', username: 'chåk7548', email: 'christofer.åkesson@dsv.su.se' }, + { id: 57, name: 'Agnes Håkansson', username: 'aghå2971', email: 'agnes.håkansson@dsv.su.se' }, + { id: 58, name: 'Sebastian Blomberg', username: 'sebl6394', email: 'sebastian.blomberg@dsv.su.se' }, + { id: 59, name: 'Felicia Gunnarsson', username: 'fegu8756', email: 'felicia.gunnarsson@dsv.su.se' }, + { id: 60, name: 'Jesper Lindahl', username: 'jeli3182', email: 'jesper.lindahl@dsv.su.se' }, + { id: 61, name: 'Sandra Backström', username: 'saba7425', email: 'sandra.backström@dsv.su.se' }, + { id: 62, name: 'William Viklund', username: 'wivi5697', email: 'william.viklund@dsv.su.se' }, + { id: 63, name: 'Lova Arvidsson', username: 'loar9231', email: 'lova.arvidsson@dsv.su.se' }, + { id: 64, name: 'Gabriel Öhman', username: 'gaöh4568', email: 'gabriel.öhman@dsv.su.se' }, + { id: 65, name: 'Isabelle Eriksson', username: 'iser8143', email: 'isabelle.eriksson@dsv.su.se' }, + { id: 66, name: 'Anton Ström', username: 'anst2976', email: 'anton.ström@dsv.su.se' }, + { id: 67, name: 'Wilma Ljungberg', username: 'wilj6314', email: 'wilma.ljungberg@dsv.su.se' }, + { id: 68, name: 'Lucas Sundström', username: 'lusu9587', email: 'lucas.sundström@dsv.su.se' }, + { id: 69, name: 'Hanna Åslund', username: 'haås4729', email: 'hanna.åslund@dsv.su.se' }, + { id: 70, name: 'Pontus Rydberg', username: 'pory8152', email: 'pontus.rydberg@dsv.su.se' }, + { id: 71, name: 'Olivia Nyman', username: 'olny3675', email: 'olivia.nyman@dsv.su.se' }, + { id: 72, name: 'Viktor Östberg', username: 'viös7493', email: 'viktor.östberg@dsv.su.se' }, + { id: 73, name: 'Tilda Forslund', username: 'tifo5128', email: 'tilda.forslund@dsv.su.se' }, + { id: 74, name: 'Carl Holmström', username: 'caho8916', email: 'carl.holmström@dsv.su.se' }, + { id: 75, name: 'Matilda Bengtsson', username: 'mabe4382', email: 'matilda.bengtsson@dsv.su.se' }, + { id: 76, name: 'Alvin Berglund', username: 'albe7654', email: 'alvin.berglund@dsv.su.se' }, + { id: 77, name: 'Saga Nordström', username: 'sano2496', email: 'saga.nordström@dsv.su.se' }, + { id: 78, name: 'Linus Hedström', username: 'lihe6847', email: 'linus.hedström@dsv.su.se' }, + { id: 79, name: 'Elina Jakobsson', username: 'elja9173', email: 'elina.jakobsson@dsv.su.se' }, + { id: 80, name: 'Casper Nordin', username: 'cano3521', email: 'casper.nordin@dsv.su.se' }, + { id: 81, name: 'Nova Malmberg', username: 'noma8765', email: 'nova.malmberg@dsv.su.se' }, + { id: 82, name: 'Isac Björk', username: 'isbj5194', email: 'isac.björk@dsv.su.se' }, + { id: 83, name: 'Ebba Sandström', username: 'ebsa7428', email: 'ebba.sandström@dsv.su.se' }, + { id: 84, name: 'Melvin Åberg', username: 'meåb2683', email: 'melvin.åberg@dsv.su.se' }, + { id: 85, name: 'Astrid Nordahl', username: 'asno6159', email: 'astrid.nordahl@dsv.su.se' }, + { id: 86, name: 'Noel Sjögren', username: 'nosj8437', email: 'noel.sjögren@dsv.su.se' }, + { id: 87, name: 'Linnéa Borg', username: 'libo4826', email: 'linnéa.borg@dsv.su.se' }, + { id: 88, name: 'Adrian Rosén', username: 'adro7195', email: 'adrian.rosén@dsv.su.se' }, + { id: 89, name: 'Smilla Lindberg', username: 'smli3564', email: 'smilla.lindberg@dsv.su.se' }, + { id: 90, name: 'Leon Hammar', username: 'leha9821', email: 'leon.hammar@dsv.su.se' }, + { id: 91, name: 'Ellen Sjöström', username: 'elsj6247', email: 'ellen.sjöström@dsv.su.se' }, + { id: 92, name: 'Tim Hedlund', username: 'tihe4573', email: 'tim.hedlund@dsv.su.se' }, + { id: 93, name: 'Vera Blomgren', username: 'vebl8912', email: 'vera.blomgren@dsv.su.se' }, + { id: 94, name: 'Theodor Larsson', username: 'thla2738', email: 'theodor.larsson@dsv.su.se' }, + { id: 95, name: 'Stella Lundström', username: 'stlu6495', email: 'stella.lundström@dsv.su.se' }, + { id: 96, name: 'Benjamin Engberg', username: 'been8164', email: 'benjamin.engberg@dsv.su.se' }, + { id: 97, name: 'Alicia Rydberg', username: 'alry4827', email: 'alicia.rydberg@dsv.su.se' }, + { id: 98, name: 'Hugo Nordgren', username: 'huno7392', email: 'hugo.nordgren@dsv.su.se' }, + { id: 99, name: 'Moa Stenberg', username: 'most5618', email: 'moa.stenberg@dsv.su.se' }, + { id: 100, name: 'Neo Lindahl', username: 'neli9456', email: 'neo.lindahl@dsv.su.se' }, + { id: 101, name: 'Tess Holm', username: 'teho3274', email: 'tess.holm@dsv.su.se' }, + { id: 102, name: 'Vincent Lundberg', username: 'vilu8593', email: 'vincent.lundberg@dsv.su.se' }, + { id: 103, name: 'Tove Nyberg', username: 'tony6127', email: 'tove.nyberg@dsv.su.se' }, + { id: 104, name: 'Edvin Kvist', username: 'edkv4851', email: 'edvin.kvist@dsv.su.se' }, + { id: 105, name: 'Sigrid Fransson', username: 'sifr7436', email: 'sigrid.fransson@dsv.su.se' }, + { id: 106, name: 'Lovis Hedberg', username: 'lohe2984', email: 'lovis.hedberg@dsv.su.se' }, + { id: 107, name: 'Arvid Stenberg', username: 'arst5627', email: 'arvid.stenberg@dsv.su.se' }, + { id: 108, name: 'June Holmgren', username: 'juho8315', email: 'june.holmgren@dsv.su.se' }, + { id: 109, name: 'Milo Svensson', username: 'misv4762', email: 'milo.svensson@dsv.su.se' }, + { id: 110, name: 'Alice Rosén', username: 'alro6948', email: 'alice.rosén@dsv.su.se' }, + { id: 111, name: 'Viggo Lindström', username: 'vili3591', email: 'viggo.lindström@dsv.su.se' }, + { id: 112, name: 'Thea Östlund', username: 'thös8274', email: 'thea.östlund@dsv.su.se' }, + { id: 113, name: 'Nils Hedström', username: 'nihe5416', email: 'nils.hedström@dsv.su.se' }, + { id: 114, name: 'Signe Dahlberg', username: 'sida7829', email: 'signe.dahlberg@dsv.su.se' }, + { id: 115, name: 'Axel Nordström', username: 'axno2653', email: 'axel.nordström@dsv.su.se' } ]; +export const USER = { + id: 1, + name: 'Jacob Reinikainen', + username: 'jare2473', + email: 'jacob.reinikainen@dsv.su.se' +}; + export const DEFAULT_DISABLED_OPTIONS = { 1: false, 2: false, diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index 9bc39a1..ca73ae4 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -101,12 +101,12 @@ export function useBookingState(addBooking) { const handleParticipantChange = useCallback((participantId) => { console.log('handleParticipantChange called with:', participantId); if (participantId !== null && participantId !== undefined) { - // Find the person by ID and add their name + // Find the person by ID and add the full person object const person = PEOPLE.find(p => p.id === participantId); console.log('Found person:', person); - if (person && !participants.includes(person.name)) { + if (person && !participants.find(p => p.id === person.id)) { console.log('Adding participant:', person.name); - setParticipants(prev => [...prev, person.name]); + setParticipants(prev => [...prev, person]); } else { console.log('Participant already exists or person not found'); } @@ -115,7 +115,7 @@ export function useBookingState(addBooking) { }, [participants]); const handleRemoveParticipant = useCallback((participantToRemove) => { - setParticipants(prev => prev.filter(p => p !== participantToRemove)); + setParticipants(prev => prev.filter(p => p.id !== participantToRemove.id)); }, []); // Memoize the return object to prevent unnecessary re-renders -- 2.39.5 From 88cda31f7e521495903567a676e51be9902baf1e Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:18:15 +0200 Subject: [PATCH 12/41] calendar improvments --- my-app/src/components/BookingDatePicker.jsx | 28 +++++++++- my-app/src/helpers.jsx | 36 +++++++------ my-app/src/icons/CalendarIcon.jsx | 18 +++++++ .../src/react-aria-starter/src/DatePicker.css | 54 +++++++++++++++++-- .../src/react-aria-starter/src/DatePicker.tsx | 10 ++-- 5 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 my-app/src/icons/CalendarIcon.jsx diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index a0bf325..9412192 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -6,15 +6,39 @@ import { useBookingContext } from '../context/BookingContext'; export function BookingDatePicker() { const booking = useBookingContext(); + const minDate = today(getLocalTimeZone()); + const maxDate = getFutureDate(14); + + const handlePreviousDay = () => { + const previousDay = booking.selectedDate.subtract({ days: 1 }); + if (previousDay.compare(minDate) >= 0) { + booking.handleDateChange(previousDay); + } + }; + + const handleNextDay = () => { + const nextDay = booking.selectedDate.add({ days: 1 }); + if (nextDay.compare(maxDate) <= 0) { + booking.handleDateChange(nextDay); + } + }; + + const canNavigatePrevious = booking.selectedDate.compare(minDate) > 0; + const canNavigateNext = booking.selectedDate.compare(maxDate) < 0; + return (
booking.handleDateChange(date)} firstDayOfWeek="mon" - minValue={today(getLocalTimeZone())} - maxValue={getFutureDate(14)} + minValue={minDate} + maxValue={maxDate} isDateUnavailable={isDateUnavailable} + onPreviousClick={handlePreviousDay} + onNextClick={handleNextDay} + canNavigatePrevious={canNavigatePrevious} + canNavigateNext={canNavigateNext} />
); diff --git a/my-app/src/helpers.jsx b/my-app/src/helpers.jsx index 57f6d84..e48ed55 100644 --- a/my-app/src/helpers.jsx +++ b/my-app/src/helpers.jsx @@ -11,21 +11,27 @@ export function getTimeFromIndex(timeIndex) { } export function convertDateObjectToString( date ) { - - const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; const dayIndex = getDayOfWeek(date, "en-US"); - - const dayOfWeek = (dayIndex >= 1 && dayIndex <= 7) ? days[dayIndex - 1] : "Ogiltig dag"; - - const months = [ - "Januari", "Februari", "Mars", "April", "Maj", "Juni", - "Juli", "Augusti", "September", "Oktober", "November", "December" - ]; - const monthIndex = date.month; - const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; - - - return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`; - return ""; + + // Always use long format for now + const isSmallScreen = false; + + if (isSmallScreen) { + const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"]; + const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"]; + + const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag"; + const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; + + return `${dayOfWeek} ${date.day} ${monthName}`; + } else { + const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; + const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"]; + + const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag"; + const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad"; + + return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`; + } } \ No newline at end of file diff --git a/my-app/src/icons/CalendarIcon.jsx b/my-app/src/icons/CalendarIcon.jsx new file mode 100644 index 0000000..d12ba45 --- /dev/null +++ b/my-app/src/icons/CalendarIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export default function CalendarIcon({ color = '#666', size = 16 }) { + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index de534c7..f09f5e0 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -43,12 +43,11 @@ } .chevron-button:disabled { - cursor: not-allowed; - opacity: 0.3; + cursor: default; } .chevron-button:focus-visible { - outline: 2px solid var(--focus-ring-color); + outline: 2px solid #2563EB; outline-offset: 2px; } @@ -79,6 +78,55 @@ } } + .calendar-button { + min-width: 220px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + gap: 0.75rem !important; + cursor: pointer !important; + background: white !important; + border: 1px solid #E5E7EB !important; + border-radius: 8px !important; + padding: 12px 16px !important; + font-weight: 500 !important; + color: #374151 !important; + transition: all 0.2s ease !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + white-space: nowrap !important; + } + + @media (max-width: 640px) { + .calendar-button { + min-width: 200px !important; + } + } + + .calendar-button:hover { + border-color: #D1D5DB !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; + } + + .calendar-button[data-pressed] { + background: #F9FAFB !important; + border-color: #9CA3AF !important; + transform: translateY(1px) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + } + + .calendar-button[data-focus-visible] { + outline: 2px solid #2563EB !important; + outline-offset: 2px !important; + border-color: #2563EB !important; + } + + .calendar-date { + flex: 1; + text-align: left; + font-size: 14px; + line-height: 1.25; + } + .react-aria-DateInput { padding: 4px 2.5rem 4px 8px; } diff --git a/my-app/src/react-aria-starter/src/DatePicker.tsx b/my-app/src/react-aria-starter/src/DatePicker.tsx index 4947b22..5fb9f8a 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.tsx +++ b/my-app/src/react-aria-starter/src/DatePicker.tsx @@ -23,6 +23,7 @@ import './DatePicker.css'; import { convertDateObjectToString } from '../../helpers'; import ChevronLeft from '../../icons/ChevronLeft'; import ChevronRight from '../../icons/ChevronRight'; +import CalendarIcon from '../../icons/CalendarIcon'; export interface DatePickerProps extends AriaDatePickerProps { @@ -70,18 +71,21 @@ export function DatePicker( disabled={!canNavigatePrevious} > - + -- 2.39.5 From d0e70f4b19b7431cf6cbd9125923137fcc382add Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:50:52 +0200 Subject: [PATCH 13/41] Calendar improvemts (settings for testing!) --- my-app/src/App.jsx | 9 +- my-app/src/AppRoutes.jsx | 2 + my-app/src/components/BookingDatePicker.jsx | 8 +- my-app/src/components/Header.jsx | 1 + my-app/src/context/SettingsContext.jsx | 97 +++++++ my-app/src/hooks/useBookingState.js | 4 +- my-app/src/pages/CalendarSettings.jsx | 206 +++++++++++++++ my-app/src/pages/CalendarSettings.module.css | 236 ++++++++++++++++++ my-app/src/pages/NewBooking.jsx | 4 +- .../src/react-aria-starter/src/Calendar.css | 9 + .../src/react-aria-starter/src/DatePicker.css | 2 + my-app/src/utils/bookingUtils.js | 9 +- 12 files changed, 573 insertions(+), 14 deletions(-) create mode 100644 my-app/src/context/SettingsContext.jsx create mode 100644 my-app/src/pages/CalendarSettings.jsx create mode 100644 my-app/src/pages/CalendarSettings.module.css diff --git a/my-app/src/App.jsx b/my-app/src/App.jsx index af9cdd4..dd45cca 100644 --- a/my-app/src/App.jsx +++ b/my-app/src/App.jsx @@ -1,12 +1,15 @@ import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import AppRoutes from './AppRoutes'; // move the routing and loading logic here +import { SettingsProvider } from './context/SettingsContext'; function App() { return ( - - - + + + + + ); } diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index d93aa9b..5f4f6eb 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; +import { CalendarSettings } from './pages/CalendarSettings'; import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { @@ -31,6 +32,7 @@ const AppRoutes = () => { }> } /> } /> + } /> diff --git a/my-app/src/components/BookingDatePicker.jsx b/my-app/src/components/BookingDatePicker.jsx index 9412192..7726afc 100644 --- a/my-app/src/components/BookingDatePicker.jsx +++ b/my-app/src/components/BookingDatePicker.jsx @@ -3,11 +3,13 @@ import { DatePicker } from '../react-aria-starter/src/DatePicker'; import { today, getLocalTimeZone } from '@internationalized/date'; import { getFutureDate, isDateUnavailable } from '../utils/bookingUtils'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; export function BookingDatePicker() { const booking = useBookingContext(); - const minDate = today(getLocalTimeZone()); - const maxDate = getFutureDate(14); + const { settings, getEffectiveToday } = useSettingsContext(); + const minDate = getEffectiveToday(); + const maxDate = minDate.add({ days: settings.bookingRangeDays }); const handlePreviousDay = () => { const previousDay = booking.selectedDate.subtract({ days: 1 }); @@ -34,7 +36,7 @@ export function BookingDatePicker() { firstDayOfWeek="mon" minValue={minDate} maxValue={maxDate} - isDateUnavailable={isDateUnavailable} + isDateUnavailable={(date) => isDateUnavailable(date, minDate, settings.bookingRangeDays)} onPreviousClick={handlePreviousDay} onNextClick={handleNextDay} canNavigatePrevious={canNavigatePrevious} diff --git a/my-app/src/components/Header.jsx b/my-app/src/components/Header.jsx index 5c00ecb..c9c9525 100644 --- a/my-app/src/components/Header.jsx +++ b/my-app/src/components/Header.jsx @@ -31,6 +31,7 @@ const Header = () => {
{/* Menu items */} Lokalbokning + Calendar Settings
)} diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx new file mode 100644 index 0000000..6d7d1ae --- /dev/null +++ b/my-app/src/context/SettingsContext.jsx @@ -0,0 +1,97 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { today, getLocalTimeZone, CalendarDate } from '@internationalized/date'; + +const SettingsContext = createContext(); + +export const useSettingsContext = () => { + const context = useContext(SettingsContext); + if (!context) { + throw new Error('useSettingsContext must be used within a SettingsProvider'); + } + return context; +}; + +export const SettingsProvider = ({ children }) => { + const [settings, setSettings] = useState(() => { + // Load settings from localStorage or use defaults + const saved = localStorage.getItem('calendarSettings'); + if (saved) { + try { + const parsed = JSON.parse(saved); + return { + ...parsed, + // Convert date strings back to DateValue objects + mockToday: parsed.mockToday ? new Date(parsed.mockToday) : null, + }; + } catch (e) { + console.warn('Failed to parse saved settings:', e); + } + } + + return { + // Use mock date if set, otherwise real today + mockToday: null, + // Days in the future users can book + bookingRangeDays: 14, + // Room availability percentage + roomAvailabilityChance: 0.7, + // Number of rooms + numberOfRooms: 5, + // Earliest booking time (in half-hour slots from 8:00) + earliestTimeSlot: 0, // 8:00 + // Latest booking time + latestTimeSlot: 22, // 19:00 (last slot ending at 19:30) + }; + }); + + // Save settings to localStorage whenever they change + useEffect(() => { + const toSave = { + ...settings, + // Convert Date objects to strings for JSON serialization + mockToday: settings.mockToday ? settings.mockToday.toISOString() : null, + }; + localStorage.setItem('calendarSettings', JSON.stringify(toSave)); + }, [settings]); + + // Get the effective "today" date (mock or real) + const getEffectiveToday = () => { + if (settings.mockToday) { + // Convert JavaScript Date to CalendarDate using the proper library function + const mockDate = settings.mockToday; + const year = mockDate.getFullYear(); + const month = mockDate.getMonth() + 1; // JS months are 0-indexed + const day = mockDate.getDate(); + + return new CalendarDate(year, month, day); + } + return today(getLocalTimeZone()); + }; + + const updateSettings = (newSettings) => { + setSettings(prev => ({ ...prev, ...newSettings })); + }; + + const resetSettings = () => { + setSettings({ + mockToday: null, + bookingRangeDays: 14, + roomAvailabilityChance: 0.7, + numberOfRooms: 5, + earliestTimeSlot: 0, + latestTimeSlot: 22, + }); + localStorage.removeItem('calendarSettings'); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index ca73ae4..c634f5c 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -9,7 +9,7 @@ import { import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; -export function useBookingState(addBooking) { +export function useBookingState(addBooking, initialDate = null) { // State hooks - simplified back to useState for stability const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(generateInitialRooms()); const [currentRoom, setCurrentRoom] = useState(null); @@ -18,7 +18,7 @@ export function useBookingState(addBooking) { const [selectedEndIndex, setSelectedEndIndex] = useState(null); const [selectedBookingLength, setSelectedBookingLength] = useState(0); const [participant, setParticipant] = useState("Arjohn Emilsson"); - const [selectedDate, setSelectedDate] = useState(today(getLocalTimeZone())); + const [selectedDate, setSelectedDate] = useState(initialDate || today(getLocalTimeZone())); const [availableTimeSlots, setAvailableTimeSlots] = useState([]); const [indeciesInHover, setIndeciesInHover] = useState([]); const [title, setTitle] = useState(""); diff --git a/my-app/src/pages/CalendarSettings.jsx b/my-app/src/pages/CalendarSettings.jsx new file mode 100644 index 0000000..fdb7a50 --- /dev/null +++ b/my-app/src/pages/CalendarSettings.jsx @@ -0,0 +1,206 @@ +import React, { useState } from 'react'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './CalendarSettings.module.css'; + +export function CalendarSettings() { + const { settings, updateSettings, resetSettings, getEffectiveToday } = useSettingsContext(); + const [tempDate, setTempDate] = useState(''); + + const handleMockDateChange = (e) => { + const dateValue = e.target.value; + setTempDate(dateValue); + if (dateValue) { + updateSettings({ mockToday: new Date(dateValue) }); + } else { + updateSettings({ mockToday: null }); + } + }; + + const formatDateForInput = (date) => { + if (!date) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + const getTimeFromSlot = (slot) => { + const totalMinutes = 8 * 60 + slot * 30; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return `${hours}:${minutes === 0 ? '00' : '30'}`; + }; + + const effectiveToday = getEffectiveToday(); + const isUsingMockDate = settings.mockToday !== null; + + return ( +
+
+

Calendar Settings

+

Configure calendar behavior for testing purposes

+
+ +
+
+

Date Settings

+ +
+ +
+ + {isUsingMockDate && ( + + )} +
+
+ Effective today: {effectiveToday.day}/{effectiveToday.month}/{effectiveToday.year} + {isUsingMockDate && (MOCK)} +
+
+ +
+ + updateSettings({ bookingRangeDays: parseInt(e.target.value) })} + className={styles.numberInput} + /> +
+ Latest bookable date: {effectiveToday.add({ days: settings.bookingRangeDays }).day}/{effectiveToday.add({ days: settings.bookingRangeDays }).month}/{effectiveToday.add({ days: settings.bookingRangeDays }).year} +
+
+
+ +
+

Room Settings

+ +
+ + updateSettings({ numberOfRooms: parseInt(e.target.value) })} + className={styles.numberInput} + /> +
+ +
+ +
+ updateSettings({ roomAvailabilityChance: parseFloat(e.target.value) })} + className={styles.slider} + /> + + {Math.round(settings.roomAvailabilityChance * 100)}% + +
+
+
+ +
+

Time Slot Settings

+ +
+ + +
+ +
+ + +
+
+ +
+ +
+ Settings are automatically saved and will persist between sessions +
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/pages/CalendarSettings.module.css b/my-app/src/pages/CalendarSettings.module.css new file mode 100644 index 0000000..dd455c2 --- /dev/null +++ b/my-app/src/pages/CalendarSettings.module.css @@ -0,0 +1,236 @@ +.container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + font-family: system-ui, -apple-system, sans-serif; +} + +.header { + margin-bottom: 2rem; + text-align: center; +} + +.header h1 { + font-size: 2.5rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 0.5rem; +} + +.header p { + color: #6b7280; + font-size: 1.1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.section { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.section h2 { + font-size: 1.5rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 1.5rem; + border-bottom: 2px solid #f3f4f6; + padding-bottom: 0.5rem; +} + +.setting { + margin-bottom: 1.5rem; +} + +.setting:last-child { + margin-bottom: 0; +} + +.setting label { + display: block; + margin-bottom: 0.5rem; +} + +.setting label strong { + font-size: 1rem; + font-weight: 600; + color: #374151; + display: block; +} + +.description { + font-size: 0.875rem; + color: #6b7280; + margin-top: 0.25rem; + display: block; +} + +.dateGroup { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.dateInput, .numberInput, .select { + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background: white; +} + +.dateInput:focus, .numberInput:focus, .select:focus { + outline: none; + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.dateInput { + width: 200px; +} + +.numberInput { + width: 120px; +} + +.select { + width: 150px; +} + +.clearButton { + padding: 0.5rem 1rem; + background: #f3f4f6; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + color: #374151; + cursor: pointer; + transition: all 0.2s; +} + +.clearButton:hover { + background: #e5e7eb; + border-color: #9ca3af; +} + +.currentStatus { + margin-top: 0.5rem; + font-size: 0.875rem; + color: #4b5563; +} + +.mockLabel { + background: #fbbf24; + color: #92400e; + padding: 0.125rem 0.375rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.sliderGroup { + display: flex; + align-items: center; + gap: 1rem; +} + +.slider { + flex: 1; + height: 6px; + border-radius: 3px; + background: #e5e7eb; + outline: none; + cursor: pointer; + -webkit-appearance: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #2563eb; + cursor: pointer; + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.slider::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: #2563eb; + cursor: pointer; + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.sliderValue { + font-weight: 600; + color: #2563eb; + min-width: 40px; + text-align: center; +} + +.actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 2rem; + background: #f9fafb; + border-radius: 12px; + border: 1px solid #e5e7eb; +} + +.resetButton { + padding: 0.75rem 2rem; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; +} + +.resetButton:hover { + background: #b91c1c; +} + +.info { + font-size: 0.875rem; + color: #6b7280; + text-align: center; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .section { + padding: 1rem; + } + + .dateGroup { + flex-direction: column; + align-items: flex-start; + } + + .sliderGroup { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index f226bad..0affa56 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -8,9 +8,11 @@ import { RoomSelectionField } from '../components/RoomSelectionField'; import { BookingLengthField } from '../components/BookingLengthField'; import { useBookingState } from '../hooks/useBookingState'; import { BookingProvider } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; export function NewBooking({ addBooking }) { - const booking = useBookingState(addBooking); + const { getEffectiveToday } = useSettingsContext(); + const booking = useBookingState(addBooking, getEffectiveToday()); return ( diff --git a/my-app/src/react-aria-starter/src/Calendar.css b/my-app/src/react-aria-starter/src/Calendar.css index 0b91bf7..3a9e38d 100644 --- a/my-app/src/react-aria-starter/src/Calendar.css +++ b/my-app/src/react-aria-starter/src/Calendar.css @@ -23,6 +23,10 @@ width: 2rem; height: 2rem; padding: 0; + + &[data-disabled] { + display: none; + } } .react-aria-CalendarCell { @@ -39,6 +43,10 @@ display: none; } + &:hover:not([data-selected]):not([data-disabled]):not([data-unavailable]) { + background-color: #e9e9e9; + } + &[data-pressed] { background: var(--gray-100); } @@ -64,6 +72,7 @@ &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); + color: rgb(203, 203, 203); } } diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css index f09f5e0..5e17c75 100644 --- a/my-app/src/react-aria-starter/src/DatePicker.css +++ b/my-app/src/react-aria-starter/src/DatePicker.css @@ -134,6 +134,8 @@ .react-aria-Popover[data-trigger=DatePicker] { max-width: unset; + transform: translateX(-50%); + left: 50% !important; } .react-aria-DatePicker { diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index fb81d9b..4832898 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -74,15 +74,14 @@ export const getLongestConsecutive = (allRooms) => { return longest; }; -export const createDisabledRanges = () => { - const now = today(getLocalTimeZone()); +export const createDisabledRanges = (effectiveToday, bookingRangeDays = 14) => { return [ - [now.add({ days: 14 }), now.add({ days: 9999 })], + [effectiveToday.add({ days: bookingRangeDays }), effectiveToday.add({ days: 9999 })], ]; }; -export const isDateUnavailable = (date) => { - const disabledRanges = createDisabledRanges(); +export const isDateUnavailable = (date, effectiveToday, bookingRangeDays = 14) => { + const disabledRanges = createDisabledRanges(effectiveToday, bookingRangeDays); return disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 -- 2.39.5 From 1bbe6f73d751d6b36c1c87b1a055a5bde9269fb5 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:06:06 +0200 Subject: [PATCH 14/41] Improvements! whohoo --- my-app/src/AppRoutes.jsx | 4 +- my-app/src/components/BookingModal.jsx | 88 +++++++++++++++++++ my-app/src/components/Header.jsx | 2 +- my-app/src/components/RoomSelectionField.jsx | 14 ++- my-app/src/components/TimeCard.jsx | 83 ++--------------- my-app/src/hooks/useBookingState.js | 21 ++++- ...lendarSettings.jsx => BookingSettings.jsx} | 8 +- ....module.css => BookingSettings.module.css} | 0 my-app/src/utils/bookingUtils.js | 5 +- 9 files changed, 137 insertions(+), 88 deletions(-) create mode 100644 my-app/src/components/BookingModal.jsx rename my-app/src/pages/{CalendarSettings.jsx => BookingSettings.jsx} (97%) rename my-app/src/pages/{CalendarSettings.module.css => BookingSettings.module.css} (100%) diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 5f4f6eb..db08a10 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; -import { CalendarSettings } from './pages/CalendarSettings'; +import { BookingSettings } from './pages/BookingSettings'; import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { @@ -32,7 +32,7 @@ const AppRoutes = () => { }> } /> } /> - } /> + } /> diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx new file mode 100644 index 0000000..435ddd8 --- /dev/null +++ b/my-app/src/components/BookingModal.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { Button, Dialog, Heading, Modal } from 'react-aria-components'; +import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; +import Dropdown from './Dropdown'; +import { useBookingContext } from '../context/BookingContext'; +import styles from './TimeCard.module.css'; + +export function BookingModal({ + startTimeIndex, + hoursAvailable, + endTimeIndex, + setEndTimeIndex, + className +}) { + const booking = useBookingContext(); + + 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" }, + ]; + + function getLabelFromAvailableHours(availableHours) { + return bookingLengths.find(option => option.value === availableHours)?.label || "Välj längd"; + } + + 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 handleChange(event) { + console.log(event.target.value); + setEndTimeIndex(startTimeIndex + parseInt(event.target.value)); + booking.setSelectedEndIndex(startTimeIndex + parseInt(event.target.value)); + } + + return ( + + +
+ {booking.title == "" ? "Jacobs bokning" : booking.title} +

{convertDateObjectToString(booking.selectedDate)}

+

{getTimeFromIndex(startTimeIndex)} - {getTimeFromIndex(endTimeIndex)}

+ +
+ + +
+ +
+ +

G5:12

+
+ +
+ +

{booking.participants.length > 0 ? booking.participants.map(p => p.name).join(", ") : "Inga deltagare"}

+
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/Header.jsx b/my-app/src/components/Header.jsx index c9c9525..b96f4e0 100644 --- a/my-app/src/components/Header.jsx +++ b/my-app/src/components/Header.jsx @@ -31,7 +31,7 @@ const Header = () => {
{/* Menu items */} Lokalbokning - Calendar Settings + Booking Settings
)} diff --git a/my-app/src/components/RoomSelectionField.jsx b/my-app/src/components/RoomSelectionField.jsx index df0d13e..1474dc4 100644 --- a/my-app/src/components/RoomSelectionField.jsx +++ b/my-app/src/components/RoomSelectionField.jsx @@ -1,17 +1,27 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Dropdown from './Dropdown'; import { SMALL_GROUP_ROOMS } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; import styles from './RoomSelectionField.module.css'; export function RoomSelectionField() { const booking = useBookingContext(); + const { settings } = useSettingsContext(); + + // Generate room options based on settings + const roomOptions = useMemo(() => { + return Array.from({ length: settings.numberOfRooms }, (_, i) => ({ + value: `G5:${i + 1}`, + label: `G5:${i + 1}`, + })); + }, [settings.numberOfRooms]); return (

Rum

booking.handleRoomChange(e)} placeholder={{ label: "Alla rum", diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index 6f60c10..d108145 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -1,12 +1,8 @@ -import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components'; - +import { Button, DialogTrigger } from 'react-aria-components'; import React, { useState } from 'react'; - import styles from './TimeCard.module.css'; - -import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; -import Dropdown from './Dropdown'; import { useBookingContext } from '../context/BookingContext'; +import { BookingModal } from './BookingModal'; export default function TimeCard({ startTimeIndex, @@ -47,37 +43,6 @@ export default function TimeCard({ const isEndState = ["availableEndTime", "selectedEnd"].includes(state); - 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" }, - ]; - - function getLabelFromAvailableHours(availableHours) { - return bookingLengths.find(option => option.value === availableHours)?.label || "Välj längd"; - } - - 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 handleChange(event) { - console.log(event.target.value); - setEndTimeIndex(startTimeIndex + parseInt(event.target.value)); - booking.setSelectedEndIndex(startTimeIndex + parseInt(event.target.value)); - } if (state === "availableSlot") { @@ -99,43 +64,13 @@ export default function TimeCard({ ) : null} - - -
- {booking.title == "" ? "Jacobs bokning" : booking.title} -

{convertDateObjectToString(booking.selectedDate)}

-

{getTimeFromIndex(startTimeIndex)} - {getTimeFromIndex(endTimeIndex)}

- -
- - -
- -
- -

G5:12

-
- -
- -

{booking.participants.join(", ")}

-
-
- - -
-
-
-
+ ); } diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index c634f5c..0abc15c 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -8,10 +8,20 @@ import { } from '../utils/bookingUtils'; import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; +import { useSettingsContext } from '../context/SettingsContext'; export function useBookingState(addBooking, initialDate = null) { + const { settings } = useSettingsContext(); + // State hooks - simplified back to useState for stability - const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(generateInitialRooms()); + const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(() => + generateInitialRooms( + settings.roomAvailabilityChance, + settings.numberOfRooms, + settings.earliestTimeSlot, + settings.latestTimeSlot + ) + ); const [currentRoom, setCurrentRoom] = useState(null); const [selectedRoom, setSelectedRoom] = useState("allRooms"); const [selectedStartIndex, setSelectedStartIndex] = useState(null); @@ -55,9 +65,14 @@ export function useBookingState(addBooking, initialDate = null) { const handleDateChange = useCallback((date) => { setSelectedDate(date); - setTimeSlotsByRoom(generateInitialRooms()); + setTimeSlotsByRoom(generateInitialRooms( + settings.roomAvailabilityChance, + settings.numberOfRooms, + settings.earliestTimeSlot, + settings.latestTimeSlot + )); resetTimeSelections(); - }, [resetTimeSelections]); + }, [resetTimeSelections, settings.roomAvailabilityChance, settings.numberOfRooms, settings.earliestTimeSlot, settings.latestTimeSlot]); const handleRoomChange = useCallback((event) => { const roomValue = event.target.value; diff --git a/my-app/src/pages/CalendarSettings.jsx b/my-app/src/pages/BookingSettings.jsx similarity index 97% rename from my-app/src/pages/CalendarSettings.jsx rename to my-app/src/pages/BookingSettings.jsx index fdb7a50..7d19d3d 100644 --- a/my-app/src/pages/CalendarSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import { useSettingsContext } from '../context/SettingsContext'; -import styles from './CalendarSettings.module.css'; +import styles from './BookingSettings.module.css'; -export function CalendarSettings() { +export function BookingSettings() { const { settings, updateSettings, resetSettings, getEffectiveToday } = useSettingsContext(); const [tempDate, setTempDate] = useState(''); @@ -37,8 +37,8 @@ export function CalendarSettings() { return (
-

Calendar Settings

-

Configure calendar behavior for testing purposes

+

Booking Settings

+

Configure booking system behavior for testing purposes

diff --git a/my-app/src/pages/CalendarSettings.module.css b/my-app/src/pages/BookingSettings.module.css similarity index 100% rename from my-app/src/pages/CalendarSettings.module.css rename to my-app/src/pages/BookingSettings.module.css diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index 4832898..4512f59 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -1,11 +1,12 @@ import { today, getLocalTimeZone } from '@internationalized/date'; import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY } from '../constants/bookingConstants'; -export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS) => { +export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 22) => { return [...Array(numberOfRooms)].map((room, index) => ({ roomId: `G5:${index + 1}`, times: Array.from({ length: 23 }, (_, i) => ({ - available: Math.random() < chanceOfAvailability ? true : false + available: i >= earliestSlot && i <= latestSlot ? + (Math.random() < chanceOfAvailability) : false })) })); }; -- 2.39.5 From 23080c0c087e31cac3fa3ac2e00c780c05b7f4ba Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:32:00 +0200 Subject: [PATCH 15/41] Time fixes --- my-app/src/components/BookingModal.jsx | 73 +++++++++++++++++++-- my-app/src/components/TimeCard.jsx | 10 ++- my-app/src/components/TimeCard.module.css | 68 +++++++++++++++++++ my-app/src/components/TimeCardContainer.jsx | 18 ++++- my-app/src/hooks/useBookingState.js | 8 +++ 5 files changed, 165 insertions(+), 12 deletions(-) diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index 435ddd8..fc072de 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Button, Dialog, Heading, Modal } from 'react-aria-components'; import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; import Dropdown from './Dropdown'; @@ -13,6 +13,24 @@ export function BookingModal({ className }) { const booking = useBookingContext(); + + // Initialize with pre-selected booking length if available + const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : null; + const [selectedLength, setSelectedLength] = useState(null); + const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex); + const hasInitialized = useRef(false); + + // Effect to handle initial setup only once when modal opens + useEffect(() => { + if (initialLength && !hasInitialized.current) { + setSelectedLength(initialLength); + const newEndTime = startTimeIndex + initialLength; + setCalculatedEndTime(newEndTime); + setEndTimeIndex(newEndTime); + booking.setSelectedEndIndex(newEndTime); + hasInitialized.current = true; + } + }, [initialLength, startTimeIndex, setEndTimeIndex, booking]); const bookingLengths = [ { value: 1, label: "30 min" }, @@ -41,18 +59,51 @@ export function BookingModal({ }; function handleChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); console.log(event.target.value); - setEndTimeIndex(startTimeIndex + parseInt(event.target.value)); - booking.setSelectedEndIndex(startTimeIndex + parseInt(event.target.value)); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = startTimeIndex + lengthValue; + setCalculatedEndTime(newEndTime); + setEndTimeIndex(newEndTime); + booking.setSelectedEndIndex(newEndTime); + } else { + // Reset to default state when placeholder is selected + setCalculatedEndTime(startTimeIndex); + setEndTimeIndex(startTimeIndex); + booking.setSelectedEndIndex(null); + } } + // Check if user has selected a booking length (including pre-selected) + const hasSelectedLength = selectedLength !== null; + + // Display time range - show calculated end time if length is selected + const displayEndTime = hasSelectedLength ? calculatedEndTime : startTimeIndex; + + return (
{booking.title == "" ? "Jacobs bokning" : booking.title}

{convertDateObjectToString(booking.selectedDate)}

-

{getTimeFromIndex(startTimeIndex)} - {getTimeFromIndex(endTimeIndex)}

+
+
+
+ + {getTimeFromIndex(startTimeIndex)} +
+
+
+ + + {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} + +
+
+
@@ -60,7 +111,11 @@ export function BookingModal({ options={bookingLengths} disabledOptions={disabledOptions} onChange={handleChange} - placeholder={{ value: hoursAvailable, label: getLabelFromAvailableHours(hoursAvailable) }} + value={selectedLength || ""} + placeholder={!initialLength ? { + value: "", + label: "Välj bokningslängd" + } : null} />
@@ -77,8 +132,12 @@ export function BookingModal({ -
diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index d108145..10904bf 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -17,7 +17,9 @@ export default function TimeCard({ const booking = useBookingContext(); let hoursText; - const halfHours = hoursAvailable; + // Use the pre-selected booking length if available, otherwise use available hours + const displayHours = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : hoursAvailable; + const halfHours = displayHours; const [endTimeIndex, setEndTimeIndex] = useState(startTimeIndex + hoursAvailable); @@ -51,7 +53,9 @@ export default function TimeCard({ diff --git a/my-app/src/components/TimeCard.module.css b/my-app/src/components/TimeCard.module.css index e7f5b8a..df1774d 100644 --- a/my-app/src/components/TimeCard.module.css +++ b/my-app/src/components/TimeCard.module.css @@ -123,4 +123,72 @@ background-color: white; width: 85%; max-width: 400px; +} + +/* New time display styles */ +.timeDisplay { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.startTime, .endTime { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.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; +} + +.timeValue.placeholder { + color: #adb5bd; + font-style: italic; + font-size: 1rem; +} + +.timeSeparator { + font-size: 1.5rem; + font-weight: 300; + color: #6c757d; + margin: 0 0.5rem; +} + +/* Disabled button styles */ +.disabledButton { + background-color: #e9ecef !important; + color: #6c757d !important; + border-color: #dee2e6 !important; + cursor: not-allowed !important; +} + +.disabledButton:hover { + background-color: #e9ecef !important; + cursor: not-allowed !important; +} + +.disabledButton:active { + background-color: #e9ecef !important; } \ No newline at end of file diff --git a/my-app/src/components/TimeCardContainer.jsx b/my-app/src/components/TimeCardContainer.jsx index 3720478..4c30e1c 100644 --- a/my-app/src/components/TimeCardContainer.jsx +++ b/my-app/src/components/TimeCardContainer.jsx @@ -74,8 +74,22 @@ export function TimeCardContainer() { /* Set time card state here: */ let timeCardState = "unavailableSlot"; - if (maxConsecutive > 0) { - timeCardState = "availableSlot"; + + // If a booking length is pre-selected, only show slots that can accommodate the exact length + if (booking.selectedBookingLength !== 0) { + // Check if this slot can accommodate the selected booking length + const actualConsecutive = booking.currentRoom ? + countConsecutiveFromSlot(booking.currentRoom.times, index) : + Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, index))); + + if (actualConsecutive >= booking.selectedBookingLength) { + timeCardState = "availableSlot"; + } + } else { + // No pre-selected length, show if any time is available + if (maxConsecutive > 0) { + timeCardState = "availableSlot"; + } } return ( diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index 0abc15c..c4a8f42 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -58,6 +58,7 @@ export function useBookingState(addBooking, initialDate = null) { }, []); const handleTimeCardClick = useCallback((startHour, hoursAvailable, roomId) => { + console.log('TimeCard clicked:', { startHour, hoursAvailable, roomId }); setSelectedStartIndex(startHour); setSelectedEndIndex(startHour + hoursAvailable); setSelectedRoom(roomId); @@ -92,6 +93,13 @@ export function useBookingState(addBooking, initialDate = null) { }, [resetTimeSelections]); const handleSave = useCallback(() => { + console.log('Saving booking with:', { + selectedStartIndex, + selectedEndIndex, + selectedRoom, + title + }); + addBooking({ id: generateId(), date: selectedDate, -- 2.39.5 From ef02a1eaa357f90f7cfa287683f042a5a8ff11b9 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:55:17 +0200 Subject: [PATCH 16/41] Booking fixes --- my-app/src/components/BookingModal.jsx | 17 ++++++++++--- my-app/src/components/TimeCard.jsx | 5 +++- my-app/src/components/TimeCard.module.css | 2 +- my-app/src/context/SettingsContext.jsx | 28 +++++++++++++++++++++ my-app/src/helpers.jsx | 6 ++++- my-app/src/pages/BookingSettings.jsx | 21 ++++++++++++++++ my-app/src/pages/BookingSettings.module.css | 8 ++++-- 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index fc072de..8d72da5 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -3,6 +3,7 @@ import { Button, Dialog, Heading, Modal } from 'react-aria-components'; import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; import Dropdown from './Dropdown'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; import styles from './TimeCard.module.css'; export function BookingModal({ @@ -13,9 +14,11 @@ export function BookingModal({ className }) { const booking = useBookingContext(); + const { getCurrentUser } = useSettingsContext(); - // Initialize with pre-selected booking length if available - const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : null; + // Initialize with pre-selected booking length if available, or auto-select if only 30 min available + const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : + (hoursAvailable === 1 ? 1 : null); // Auto-select 30 min if that's all that's available const [selectedLength, setSelectedLength] = useState(null); const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex); const hasInitialized = useRef(false); @@ -126,7 +129,15 @@ export function BookingModal({
-

{booking.participants.length > 0 ? booking.participants.map(p => p.name).join(", ") : "Inga deltagare"}

+

+ {(() => { + const currentUser = getCurrentUser(); + console.log('Current user in modal:', currentUser); + const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)]; + console.log('All participants:', allParticipants); + return allParticipants.map(p => p.name).join(", "); + })()} +

+
+

User Settings

+ +
+ + updateSettings({ currentUserName: e.target.value })} + className={styles.textInput} + placeholder="Enter your name" + /> +
+
+

Date Settings

diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css index dd455c2..8496859 100644 --- a/my-app/src/pages/BookingSettings.module.css +++ b/my-app/src/pages/BookingSettings.module.css @@ -78,7 +78,7 @@ align-items: center; } -.dateInput, .numberInput, .select { +.dateInput, .numberInput, .select, .textInput { padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; @@ -87,7 +87,7 @@ background: white; } -.dateInput:focus, .numberInput:focus, .select:focus { +.dateInput:focus, .numberInput:focus, .select:focus, .textInput:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); @@ -105,6 +105,10 @@ width: 150px; } +.textInput { + width: 250px; +} + .clearButton { padding: 0.5rem 1rem; background: #f3f4f6; -- 2.39.5 From 650915ba86c762315cd9d4c501b30cad3b8eabda Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:43:30 +0200 Subject: [PATCH 17/41] focus state on time card --- my-app/src/components/BookingModal.jsx | 2 +- my-app/src/components/BookingModal.module.css | 175 ++++++++++++++++++ my-app/src/components/TimeCard.module.css | 53 ++++-- 3 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 my-app/src/components/BookingModal.module.css diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index 8d72da5..3eb674d 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -4,7 +4,7 @@ import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; import Dropdown from './Dropdown'; import { useBookingContext } from '../context/BookingContext'; import { useSettingsContext } from '../context/SettingsContext'; -import styles from './TimeCard.module.css'; +import styles from './BookingModal.module.css'; export function BookingModal({ startTimeIndex, diff --git a/my-app/src/components/BookingModal.module.css b/my-app/src/components/BookingModal.module.css new file mode 100644 index 0000000..7156d88 --- /dev/null +++ b/my-app/src/components/BookingModal.module.css @@ -0,0 +1,175 @@ +/* TIME CARD */ + +.startTime { + width: fit-content; + font-weight: 600; + font-size: 1.3rem; +} + +.modalFooter { + height: fit-content; + width: 100%; + color: blue; + display: flex; + align-items: center; + gap: 1rem; + margin-top: 2rem; +} + + +/* MODAL */ + +.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; +} + +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; +} + +.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); +} + +.saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; +} + +.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; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.timeSpan { + font-size: 2rem; + margin: 0; +} + +.sectionWithTitle { + padding-top: 1rem; + display: flex; + flex-direction: column; + width: fit-content +} + +.sectionWithTitle label { + font-size: 0.8rem; + color: #717171; +} + +.sectionWithTitle p { + margin: 0; +} + +.modalContainer { + background-color: white; + width: 85%; + max-width: 400px; +} + +/* New time display styles */ +.timeDisplay { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + min-width: 196px; + width: fit-content; +} + +.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; +} + +.timeValue.placeholder { + color: #adb5bd; + font-style: italic; + font-size: 1rem; +} + +.timeSeparator { + font-size: 1.5rem; + font-weight: 400; + color: #6c757d; + margin: 0 0.5rem; + padding-top: 1.3rem; +} + +/* Disabled button styles */ +.disabledButton { + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; + opacity: 0.6 !important; + box-shadow: none; +} + +.disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none; + cursor: default; +} + +.disabledButton:active { + background-color: #f8f9fa !important; + transform: none !important; +} + diff --git a/my-app/src/components/TimeCard.module.css b/my-app/src/components/TimeCard.module.css index 1c694ef..2b231f5 100644 --- a/my-app/src/components/TimeCard.module.css +++ b/my-app/src/components/TimeCard.module.css @@ -19,6 +19,11 @@ background-color: #E5E5E5; } +.container[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; +} + .container p { margin: 0; } @@ -67,36 +72,49 @@ .cancelButton { flex: 2; - background-color: #ECECEC; + background-color: white; height: 4rem; - color: #5c5454; + color: #374151; font-weight: 600; - border: 1px solid #D2D9E0; + border: 2px solid #d1d5db; border-radius: 0.5rem; + transition: all 0.2s ease; } -.cancelButton:hover, -.saveButton:hover { - cursor: pointer +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; } .saveButton { flex: 3; - background-color: #4C952D; - color: #f2faef; + background-color: #059669; + color: white; height: 4rem; font-weight: 600; font-size: 1.1rem; - border: 1px solid #3C7624; + border: 2px solid #047857; border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); +} + +.saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; } .saveButton:active { - background-color: #3C7624; + background-color: #065f46; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(5, 150, 105, 0.2); } .cancelButton:active { - background-color: #D2D9E0; + background-color: #e5e7eb; + transform: translateY(1px); } .timeSpan { @@ -178,17 +196,20 @@ /* Disabled button styles */ .disabledButton { - background-color: #e9ecef !important; - color: #6c757d !important; - border-color: #dee2e6 !important; + background-color: #f8f9fa !important; + color: #adb5bd !important; + border: 2px dashed #dee2e6 !important; cursor: not-allowed !important; + opacity: 0.6 !important; } .disabledButton:hover { - background-color: #e9ecef !important; + background-color: #f8f9fa !important; cursor: not-allowed !important; + transform: none !important; } .disabledButton:active { - background-color: #e9ecef !important; + background-color: #f8f9fa !important; + transform: none !important; } \ No newline at end of file -- 2.39.5 From 9118cf11bee96fccaac5ba8b2e37b9c42936fa7c Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:07:18 +0200 Subject: [PATCH 18/41] last slot is bookable --- my-app/src/context/SettingsContext.jsx | 6 +++--- my-app/src/utils/bookingUtils.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 7917afc..fd3f155 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -26,7 +26,7 @@ export const SettingsProvider = ({ children }) => { roomAvailabilityChance: 0.7, numberOfRooms: 5, earliestTimeSlot: 0, - latestTimeSlot: 22, + latestTimeSlot: 23, currentUserName: USER.name, // Then override with saved values ...parsed, @@ -52,7 +52,7 @@ export const SettingsProvider = ({ children }) => { // Earliest booking time (in half-hour slots from 8:00) earliestTimeSlot: 0, // 8:00 // Latest booking time - latestTimeSlot: 22, // 19:00 (last slot ending at 19:30) + latestTimeSlot: 23, // 19:30 (last slot ending at 20:00) // Current user settings currentUserName: USER.name, }; @@ -93,7 +93,7 @@ export const SettingsProvider = ({ children }) => { roomAvailabilityChance: 0.7, numberOfRooms: 5, earliestTimeSlot: 0, - latestTimeSlot: 22, + latestTimeSlot: 23, currentUserName: USER.name, }); localStorage.removeItem('calendarSettings'); diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js index 4512f59..d8537a2 100644 --- a/my-app/src/utils/bookingUtils.js +++ b/my-app/src/utils/bookingUtils.js @@ -1,10 +1,10 @@ import { today, getLocalTimeZone } from '@internationalized/date'; import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY } from '../constants/bookingConstants'; -export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 22) => { +export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 23) => { return [...Array(numberOfRooms)].map((room, index) => ({ roomId: `G5:${index + 1}`, - times: Array.from({ length: 23 }, (_, i) => ({ + times: Array.from({ length: 24 }, (_, i) => ({ available: i >= earliestSlot && i <= latestSlot ? (Math.random() < chanceOfAvailability) : false })) -- 2.39.5 From e133a796f90483c587050123f5ad690e84bd376a Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:38:02 +0200 Subject: [PATCH 19/41] modal overflow fix --- my-app/src/components/BookingModal.module.css | 49 ++++++++++++++----- my-app/src/components/TimeCard.module.css | 49 +++++++++++++------ .../components/TimeCardContainer.module.css | 2 + 3 files changed, 72 insertions(+), 28 deletions(-) diff --git a/my-app/src/components/BookingModal.module.css b/my-app/src/components/BookingModal.module.css index 7156d88..24b9da0 100644 --- a/my-app/src/components/BookingModal.module.css +++ b/my-app/src/components/BookingModal.module.css @@ -30,10 +30,12 @@ transition: all 0.2s ease; } -.cancelButton:hover { - background-color: #f9fafb; - border-color: #9ca3af; - cursor: pointer; +@media (hover: hover) { + .cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; + } } .saveButton { @@ -49,10 +51,12 @@ box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); } -.saveButton:hover { - background-color: #047857; - box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); - cursor: pointer; +@media (hover: hover) { + .saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; + } } .saveButton:active { @@ -99,6 +103,23 @@ max-width: 400px; } +/* Ensure modal appears above header and handles overflow */ +:global(.react-aria-ModalOverlay) { + z-index: 1100 !important; + overflow-y: auto !important; + display: flex !important; + align-items: safe center !important; + justify-content: center !important; + padding: 2rem 1rem !important; + box-sizing: border-box !important; +} + +:global(.react-aria-ModalOverlay .react-aria-Modal) { + max-height: calc(100vh - 4rem) !important; + max-width: 90vw !important; + overflow-y: auto !important; +} + /* New time display styles */ .timeDisplay { margin: 1rem 0; @@ -161,11 +182,13 @@ box-shadow: none; } -.disabledButton:hover { - background-color: #f8f9fa !important; - transform: none !important; - box-shadow: none; - cursor: default; +@media (hover: hover) { + .disabledButton:hover { + background-color: #f8f9fa !important; + transform: none !important; + box-shadow: none; + cursor: default; + } } .disabledButton:active { diff --git a/my-app/src/components/TimeCard.module.css b/my-app/src/components/TimeCard.module.css index 2b231f5..2bc1f44 100644 --- a/my-app/src/components/TimeCard.module.css +++ b/my-app/src/components/TimeCard.module.css @@ -13,13 +13,24 @@ background-color: #F8FBFC; width: 135px; height: 20px; + transition: all 0.15s ease; } -.container:hover { - background-color: #E5E5E5; +@media (hover: hover) { + .container:hover { + background-color: #E5E5E5; + } } -.container[data-focused] { +.container:active, +.container[data-pressed] { + background-color: #D1D5DB; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.1s ease; +} + +.container[data-focus-visible] { outline: 2px solid #2563EB; outline-offset: -1px; } @@ -37,11 +48,13 @@ .upToText { font-weight: 300; color: #919191; + font-size: 0.9rem } .hoursText { font-weight: 500; color:#686765; + font-size: 0.9rem; } @@ -81,10 +94,12 @@ transition: all 0.2s ease; } -.cancelButton:hover { - background-color: #f9fafb; - border-color: #9ca3af; - cursor: pointer; +@media (hover: hover) { + .cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; + cursor: pointer; + } } .saveButton { @@ -100,10 +115,12 @@ box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); } -.saveButton:hover { - background-color: #047857; - box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); - cursor: pointer; +@media (hover: hover) { + .saveButton:hover { + background-color: #047857; + box-shadow: 0 4px 8px rgba(5, 150, 105, 0.3); + cursor: pointer; + } } .saveButton:active { @@ -203,10 +220,12 @@ opacity: 0.6 !important; } -.disabledButton:hover { - background-color: #f8f9fa !important; - cursor: not-allowed !important; - transform: none !important; +@media (hover: hover) { + .disabledButton:hover { + background-color: #f8f9fa !important; + cursor: not-allowed !important; + transform: none !important; + } } .disabledButton:active { diff --git a/my-app/src/components/TimeCardContainer.module.css b/my-app/src/components/TimeCardContainer.module.css index 5e44012..012d802 100644 --- a/my-app/src/components/TimeCardContainer.module.css +++ b/my-app/src/components/TimeCardContainer.module.css @@ -13,6 +13,8 @@ width: 350px; gap: 0.5rem; height: fit-content; + align-items: center; + justify-content: center; } .timeCardList { -- 2.39.5 From 49f974adf6d33417c20c135dc7ec8c340ace53c8 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:59:21 +0200 Subject: [PATCH 20/41] modal styling --- my-app/src/components/BookingModal.jsx | 4 ++-- my-app/src/components/BookingModal.module.css | 19 +++++++++++++++---- my-app/src/react-aria-starter/src/Modal.css | 1 - 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index 3eb674d..e68c19d 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -87,8 +87,8 @@ export function BookingModal({ return ( - - + +
{booking.title == "" ? "Jacobs bokning" : booking.title}

{convertDateObjectToString(booking.selectedDate)}

diff --git a/my-app/src/components/BookingModal.module.css b/my-app/src/components/BookingModal.module.css index 24b9da0..7865535 100644 --- a/my-app/src/components/BookingModal.module.css +++ b/my-app/src/components/BookingModal.module.css @@ -98,9 +98,17 @@ } .modalContainer { - background-color: white; + 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; } /* Ensure modal appears above header and handles overflow */ @@ -112,12 +120,16 @@ justify-content: center !important; padding: 2rem 1rem !important; box-sizing: border-box !important; + background: rgba(0, 0, 0, 0.25) !important; + backdrop-filter: blur(12px) saturate(150%) !important; + -webkit-backdrop-filter: blur(12px) saturate(150%) !important; } -:global(.react-aria-ModalOverlay .react-aria-Modal) { +:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal) { max-height: calc(100vh - 4rem) !important; max-width: 90vw !important; overflow-y: auto !important; + background: rgba(255, 255, 255, 0.85) !important; } /* New time display styles */ @@ -194,5 +206,4 @@ .disabledButton:active { background-color: #f8f9fa !important; transform: none !important; -} - +} \ No newline at end of file diff --git a/my-app/src/react-aria-starter/src/Modal.css b/my-app/src/react-aria-starter/src/Modal.css index f8fec11..589190d 100644 --- a/my-app/src/react-aria-starter/src/Modal.css +++ b/my-app/src/react-aria-starter/src/Modal.css @@ -25,7 +25,6 @@ .react-aria-Modal { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); - border-radius: 6px; background: var(--overlay-background); color: var(--text-color); border: 1px solid var(--gray-400); -- 2.39.5 From 319a99b675fc236a5b9d3b123e8f540ef12d74d4 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:32:19 +0200 Subject: [PATCH 21/41] more participants and small bug fix --- my-app/src/components/BookingModal.jsx | 2 - .../src/components/ParticipantsSelector.jsx | 19 ++- my-app/src/constants/bookingConstants.js | 147 +++++++++++++++++- my-app/src/context/SettingsContext.jsx | 2 - 4 files changed, 163 insertions(+), 7 deletions(-) diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index e68c19d..c88b9f4 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -132,9 +132,7 @@ export function BookingModal({

{(() => { const currentUser = getCurrentUser(); - console.log('Current user in modal:', currentUser); const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)]; - console.log('All participants:', allParticipants); return allParticipants.map(p => p.name).join(", "); })()}

diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index d32fafd..61d7ea5 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -9,12 +9,13 @@ export function ParticipantsSelector() { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const [recentSearches, setRecentSearches] = useState([ - { id: 1, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }, { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' }, { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' } ]); const inputRef = useRef(null); const dropdownRef = useRef(null); + const itemRefs = useRef([]); // Filter people based on search term const filteredPeople = PEOPLE.filter(person => @@ -51,6 +52,16 @@ export function ParticipantsSelector() { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + // Scroll focused item into view + useEffect(() => { + if (focusedIndex >= 0 && itemRefs.current[focusedIndex]) { + itemRefs.current[focusedIndex].scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + } + }, [focusedIndex]); + const handleInputFocus = () => { // Don't auto-open dropdown on focus - wait for user interaction setFocusedIndex(-1); @@ -59,12 +70,16 @@ export function ParticipantsSelector() { const handleInputClick = () => { setIsDropdownOpen(true); setFocusedIndex(-1); + // Clear refs when dropdown opens + itemRefs.current = []; }; const handleInputChange = (e) => { setSearchTerm(e.target.value); setIsDropdownOpen(true); setFocusedIndex(-1); + // Clear refs when content changes + itemRefs.current = []; }; const handleKeyDown = (e) => { @@ -176,6 +191,7 @@ export function ParticipantsSelector() {
itemRefs.current[index] = el} className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`} onClick={() => handleSelectPerson(person)} role="option" @@ -211,6 +227,7 @@ export function ParticipantsSelector() {
itemRefs.current[index] = el} className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`} onClick={() => handleSelectPerson(person)} role="option" diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js index 19c6b2b..0627e0c 100644 --- a/my-app/src/constants/bookingConstants.js +++ b/my-app/src/constants/bookingConstants.js @@ -19,7 +19,7 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({ })); export const PEOPLE = [ - { id: 1, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }, + { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@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' }, @@ -133,7 +133,150 @@ export const PEOPLE = [ { id: 112, name: 'Thea Östlund', username: 'thös8274', email: 'thea.östlund@dsv.su.se' }, { id: 113, name: 'Nils Hedström', username: 'nihe5416', email: 'nils.hedström@dsv.su.se' }, { id: 114, name: 'Signe Dahlberg', username: 'sida7829', email: 'signe.dahlberg@dsv.su.se' }, - { id: 115, name: 'Axel Nordström', username: 'axno2653', email: 'axel.nordström@dsv.su.se' } + { id: 115, name: 'Axel Nordström', username: 'axno2653', email: 'axel.nordström@dsv.su.se' }, + { id: 116, name: 'Amira Al-Hassan', username: 'amal4821', email: 'amira.al-hassan@dsv.su.se' }, + { id: 117, name: 'José García Martínez', username: 'joga7395', email: 'jose.garcia-martinez@dsv.su.se' }, + { id: 118, name: 'Fatima Özkan', username: 'faöz2648', email: 'fatima.özkan@dsv.su.se' }, + { id: 119, name: 'Ahmed Ben Khalil', username: 'ahbe5973', email: 'ahmed.ben-khalil@dsv.su.se' }, + { id: 120, name: 'Elena Popović', username: 'elpo8416', email: 'elena.popovic@dsv.su.se' }, + { id: 121, name: 'Hassan Al-Rashid', username: 'haal3672', email: 'hassan.al-rashid@dsv.su.se' }, + { id: 122, name: 'Mariam Yılmaz', username: 'mayl9254', email: 'mariam.yilmaz@dsv.su.se' }, + { id: 123, name: 'Nicolas Müller Schmidt', username: 'nimu4817', email: 'nicolas.muller-schmidt@dsv.su.se' }, + { id: 124, name: 'Layla Abdallah', username: 'laab6539', email: 'layla.abdallah@dsv.su.se' }, + { id: 125, name: 'Dragan Milosević', username: 'drmi8142', email: 'dragan.milosevic@dsv.su.se' }, + { id: 126, name: 'Aïsha Benali', username: 'aibe2785', email: 'aisha.benali@dsv.su.se' }, + { id: 127, name: 'Omar El-Khoury', username: 'omkh5396', email: 'omar.el-khoury@dsv.su.se' }, + { id: 128, name: 'Željana Petković', username: 'žepe7614', email: 'zeljana.petkovic@dsv.su.se' }, + { id: 129, name: 'Mohammed Al-Zahra', username: 'moal3928', email: 'mohammed.al-zahra@dsv.su.se' }, + { id: 130, name: 'Francesca Di Marco', username: 'frdi6157', email: 'francesca.di-marco@dsv.su.se' }, + { id: 131, name: 'Kemal Özdogan', username: 'keöz8473', email: 'kemal.ozdogan@dsv.su.se' }, + { id: 132, name: 'Rania Mansour', username: 'rama4692', email: 'rania.mansour@dsv.su.se' }, + { id: 133, name: 'Miloš Jovanović', username: 'mijo7285', email: 'milos.jovanovic@dsv.su.se' }, + { id: 134, name: 'Yasmina Benchaib', username: 'yabe5814', email: 'yasmina.benchaib@dsv.su.se' }, + { id: 135, name: 'Andrés López Rivera', username: 'anlo3647', email: 'andres.lopez-rivera@dsv.su.se' }, + { id: 136, name: 'Nour Al-Mahmoud', username: 'noal9172', email: 'nour.al-mahmoud@dsv.su.se' }, + { id: 137, name: 'Goran Nikolić', username: 'goni6428', email: 'goran.nikolic@dsv.su.se' }, + { id: 138, name: 'Ines García López', username: 'inga4753', email: 'ines.garcia-lopez@dsv.su.se' }, + { id: 139, name: 'Yusuf Çelik', username: 'yuçe8596', email: 'yusuf.celik@dsv.su.se' }, + { id: 140, name: 'Maryam Hosseini', username: 'maho2319', email: 'maryam.hosseini@dsv.su.se' }, + { id: 141, name: 'Stjepan Kovačević', username: 'stko7684', email: 'stjepan.kovacevic@dsv.su.se' }, + { id: 142, name: 'Samira Bouzid', username: 'sabo5127', email: 'samira.bouzid@dsv.su.se' }, + { id: 143, name: 'Dimitri Papadopoulos', username: 'dipa8941', email: 'dimitri.papadopoulos@dsv.su.se' }, + { id: 144, name: 'Leïla Benabbas', username: 'lebe3465', email: 'leila.benabbas@dsv.su.se' }, + { id: 145, name: 'Aleksandar Stanković', username: 'alst6782', email: 'aleksandar.stankovic@dsv.su.se' }, + { id: 146, name: 'Carmen Ruiz Vega', username: 'caru9218', email: 'carmen.ruiz-vega@dsv.su.se' }, + { id: 147, name: 'Tariq Al-Masri', username: 'taal4576', email: 'tariq.al-masri@dsv.su.se' }, + { id: 148, name: 'Nataša Đorđević', username: 'naðo7839', email: 'natasa.djordjevic@dsv.su.se' }, + { id: 149, name: 'Ibrahim Kaya', username: 'ibka2153', email: 'ibrahim.kaya@dsv.su.se' }, + { id: 150, name: 'Soraya Ahmadi', username: 'soah5694', email: 'soraya.ahmadi@dsv.su.se' }, + { id: 151, name: 'Nikola Bogdanović', username: 'nibo8327', email: 'nikola.bogdanovic@dsv.su.se' }, + { id: 152, name: 'Lucía Hernández Silva', username: 'luhe6471', email: 'lucia.hernandez-silva@dsv.su.se' }, + { id: 153, name: 'Mehmet Güler', username: 'megu3795', email: 'mehmet.guler@dsv.su.se' }, + { id: 154, name: 'Zahra Mortazavi', username: 'zamo9148', email: 'zahra.mortazavi@dsv.su.se' }, + { id: 155, name: 'Petar Živković', username: 'peži4672', email: 'petar.zivkovic@dsv.su.se' }, + { id: 156, name: 'Inés Moreno Castro', username: 'inmo7286', email: 'ines.moreno-castro@dsv.su.se' }, + { id: 157, name: 'Salam Al-Faraj', username: 'saal5539', email: 'salam.al-faraj@dsv.su.se' }, + { id: 158, name: 'Tijana Milić', username: 'timi8713', email: 'tijana.milic@dsv.su.se' }, + { id: 159, name: 'Raúl Jiménez Torres', username: 'raji2467', email: 'raul.jimenez-torres@dsv.su.se' }, + { id: 160, name: 'Bana Al-Qasemi', username: 'baal6824', email: 'bana.al-qasemi@dsv.su.se' }, + { id: 161, name: 'Marko Simić', username: 'masi4195', email: 'marko.simic@dsv.su.se' }, + { id: 162, name: 'Álvaro González Díaz', username: 'algo7658', email: 'alvaro.gonzalez-diaz@dsv.su.se' }, + { id: 163, name: 'Hala Kassem', username: 'haka3912', email: 'hala.kassem@dsv.su.se' }, + { id: 164, name: 'Damir Petrović', username: 'dape9275', email: 'damir.petrovic@dsv.su.se' }, + { id: 165, name: 'Esperanza Rivera Morales', username: 'esri5438', email: 'esperanza.rivera-morales@dsv.su.se' }, + { id: 166, name: 'Rashid Al-Nouri', username: 'raal8164', email: 'rashid.al-nouri@dsv.su.se' }, + { id: 167, name: 'Milica Vuković', username: 'mivu6729', email: 'milica.vukovic@dsv.su.se' }, + { id: 168, name: 'Cristóbal Herrera López', username: 'crhe4583', email: 'cristobal.herrera-lopez@dsv.su.se' }, + { id: 169, name: 'Amina Benali', username: 'ambe7296', email: 'amina.benali@dsv.su.se' }, + { id: 170, name: 'Saša Radović', username: 'sara9851', email: 'sasa.radovic@dsv.su.se' }, + { id: 171, name: 'Adrián Vargas Ruiz', username: 'adva3467', email: 'adrian.vargas-ruiz@dsv.su.se' }, + { id: 172, name: 'Laith Al-Khouri', username: 'laal6142', email: 'laith.al-khouri@dsv.su.se' }, + { id: 173, name: 'Jovana Stojanović', username: 'jost8375', email: 'jovana.stojanovic@dsv.su.se' }, + { id: 174, name: 'Sebastián Méndez Torres', username: 'seme5698', email: 'sebastian.mendez-torres@dsv.su.se' }, + { id: 175, name: 'Dina Al-Rashid', username: 'dial2914', email: 'dina.al-rashid@dsv.su.se' }, + { id: 176, name: 'Viktor Đokić', username: 'viðo7427', email: 'viktor.djokic@dsv.su.se' }, + { id: 177, name: 'Paloma Santos García', username: 'pasa4851', email: 'paloma.santos-garcia@dsv.su.se' }, + { id: 178, name: 'Khalil Mahmoud', username: 'khma6293', email: 'khalil.mahmoud@dsv.su.se' }, + { id: 179, name: 'Anja Matić', username: 'anma9576', email: 'anja.matic@dsv.su.se' }, + { id: 180, name: 'Eduardo Ramírez Silva', username: 'edra3184', email: 'eduardo.ramirez-silva@dsv.su.se' }, + { id: 181, name: 'Yasmin Al-Qadri', username: 'yaal7629', email: 'yasmin.al-qadri@dsv.su.se' }, + { id: 182, name: 'Nemanja Vasić', username: 'neva5472', email: 'nemanja.vasic@dsv.su.se' }, + { id: 183, name: 'Sofía Castillo Moreno', username: 'soca8196', email: 'sofia.castillo-moreno@dsv.su.se' }, + { id: 184, name: 'Fadi Haddad', username: 'faha2758', email: 'fadi.haddad@dsv.su.se' }, + { id: 185, name: 'Dragana Marković', username: 'drma6341', email: 'dragana.markovic@dsv.su.se' }, + { id: 186, name: 'Matías Herrera Vega', username: 'mahe9487', email: 'matias.herrera-vega@dsv.su.se' }, + { id: 187, name: 'Lina Al-Zahra', username: 'lial4725', email: 'lina.al-zahra@dsv.su.se' }, + { id: 188, name: 'Stefan Đurić', username: 'stðu7853', email: 'stefan.djuric@dsv.su.se' }, + { id: 189, name: 'Valentina López García', username: 'valo3619', email: 'valentina.lopez-garcia@dsv.su.se' }, + { id: 190, name: 'Samir Nasser', username: 'sana8274', email: 'samir.nasser@dsv.su.se' }, + { id: 191, name: 'Ana Milanović', username: 'anmi6527', email: 'ana.milanovic@dsv.su.se' }, + { id: 192, name: 'Gabriel Fernández Ruiz', username: 'gafe4892', email: 'gabriel.fernandez-ruiz@dsv.su.se' }, + { id: 193, name: 'Rana Al-Masri', username: 'raal7156', email: 'rana.al-masri@dsv.su.se' }, + { id: 194, name: 'Dejan Nikolić', username: 'deni3481', email: 'dejan.nikolic@dsv.su.se' }, + { id: 195, name: 'Isabella Torres Díaz', username: 'isto8675', email: 'isabella.torres-diaz@dsv.su.se' }, + { id: 196, name: 'Omar Benali', username: 'ombe5239', email: 'omar.benali@dsv.su.se' }, + { id: 197, name: 'Milena Stanković', username: 'mist9164', email: 'milena.stankovic@dsv.su.se' }, + { id: 198, name: 'Alejandro Morales Castro', username: 'almo2748', email: 'alejandro.morales-castro@dsv.su.se' }, + { id: 199, name: 'Nadia Farouk', username: 'nafa6583', email: 'nadia.farouk@dsv.su.se' }, + { id: 200, name: 'Bojan Petrović', username: 'bope7926', email: 'bojan.petrovic@dsv.su.se' }, + { id: 201, name: 'Camila Sánchez Torres', username: 'casa4371', email: 'camila.sanchez-torres@dsv.su.se' }, + { id: 202, name: 'Yousef Al-Ahmad', username: 'yoal8649', email: 'yousef.al-ahmad@dsv.su.se' }, + { id: 203, name: 'Teodora Jovanović', username: 'tejo5194', email: 'teodora.jovanovic@dsv.su.se' }, + { id: 204, name: 'Diego Herrera Martín', username: 'dihe9738', email: 'diego.herrera-martin@dsv.su.se' }, + { id: 205, name: 'Farah Al-Qasimi', username: 'faal3526', email: 'farah.al-qasimi@dsv.su.se' }, + { id: 206, name: 'Luka Mitrović', username: 'lumi7284', email: 'luka.mitrovic@dsv.su.se' }, + { id: 207, name: 'Natalia Guzmán Silva', username: 'nagu6417', email: 'natalia.guzman-silva@dsv.su.se' }, + { id: 208, name: 'Karim Al-Rashid', username: 'kaal8952', email: 'karim.al-rashid@dsv.su.se' }, + { id: 209, name: 'Maja Stojanović', username: 'mast4635', email: 'maja.stojanovic@dsv.su.se' }, + { id: 210, name: 'Emilio García Pérez', username: 'emga7158', email: 'emilio.garcia-perez@dsv.su.se' }, + { id: 211, name: 'Layla Mansouri', username: 'lama2794', email: 'layla.mansouri@dsv.su.se' }, + { id: 212, name: 'Marija Đorđević', username: 'maðo6381', email: 'marija.djordjevic@dsv.su.se' }, + { id: 213, name: 'Ricardo Vásquez López', username: 'riva9517', email: 'ricardo.vasquez-lopez@dsv.su.se' }, + { id: 214, name: 'Zeinab Al-Khoury', username: 'zeal5743', email: 'zeinab.al-khoury@dsv.su.se' }, + { id: 215, name: 'Filip Kostić', username: 'fiko8296', email: 'filip.kostic@dsv.su.se' }, + + // Really long but realistic names + { id: 216, name: 'María Esperanza del Carmen Rodríguez-Fernández y Pérez-González', username: 'maes4829', email: 'maria.esperanza.rodriguez-fernandez@dsv.su.se' }, + { id: 217, name: 'Alexandros Konstantinos Dimitrios Papadopoulos-Georgiou', username: 'alko7395', email: 'alexandros.konstantinos.papadopoulos@dsv.su.se' }, + { id: 218, name: 'Jean-Baptiste François-Xavier de la Croix-Montmorency', username: 'jefr2648', email: 'jean-baptiste.de-la-croix@dsv.su.se' }, + { id: 219, name: 'Ana-María Concepción Guadalupe Hernández-Vásquez de los Santos', username: 'anco5973', email: 'ana-maria.hernandez-vasquez@dsv.su.se' }, + { id: 220, name: 'Christophoros Theodoros Athanasios Michalopoulos-Stavropoulos', username: 'chth8416', email: 'christophoros.michalopoulos@dsv.su.se' }, + { id: 221, name: 'José María Antonio Francisco Javier García-Sánchez y Martínez-López', username: 'joma3672', email: 'jose.maria.garcia-sanchez@dsv.su.se' }, + { id: 222, name: 'Elisabeth Charlotte Francoise Marie-Antoinette von Habsburg-Lorraine', username: 'elch9254', email: 'elisabeth.von-habsburg@dsv.su.se' }, + { id: 223, name: 'Pedro Alfonso Ramón Fernando Díez-Canseco y Herrera-Mendoza', username: 'peal4817', email: 'pedro.alfonso.diez-canseco@dsv.su.se' }, + { id: 224, name: 'Anastasia Ekaterini Theodora Constantinidou-Papadimitriou', username: 'anek6539', email: 'anastasia.constantinidou@dsv.su.se' }, + { id: 225, name: 'Francesco Giuseppe Alessandro Maria Benedetto di Savoia-Carignano', username: 'frgi8142', email: 'francesco.di-savoia@dsv.su.se' }, + + // English names + { id: 226, name: 'Oliver Pemberton-Wells', username: 'olpe2785', email: 'oliver.pemberton-wells@dsv.su.se' }, + { id: 227, name: 'Charlotte Ashworth', username: 'chas5396', email: 'charlotte.ashworth@dsv.su.se' }, + { id: 228, name: 'Henry Fitzpatrick', username: 'hefi7614', email: 'henry.fitzpatrick@dsv.su.se' }, + { id: 229, name: 'Imogen Blackwood', username: 'imbl3928', email: 'imogen.blackwood@dsv.su.se' }, + { id: 230, name: 'Rupert Cholmondeley', username: 'ruch6157', email: 'rupert.cholmondeley@dsv.su.se' }, + { id: 231, name: 'Arabella Thornbury', username: 'arth8473', email: 'arabella.thornbury@dsv.su.se' }, + { id: 232, name: 'Benedict Worthington', username: 'bewo4692', email: 'benedict.worthington@dsv.su.se' }, + { id: 233, name: 'Cordelia Featherstone', username: 'cofe7285', email: 'cordelia.featherstone@dsv.su.se' }, + { id: 234, name: 'Jasper Windermere', username: 'jawi5814', email: 'jasper.windermere@dsv.su.se' }, + { id: 235, name: 'Penelope Harrington-Lloyd', username: 'peha3647', email: 'penelope.harrington-lloyd@dsv.su.se' }, + + // Norwegian names + { id: 236, name: 'Magnus Bjørklund', username: 'mabj9172', email: 'magnus.bjorklund@dsv.su.se' }, + { id: 237, name: 'Astrid Haugen', username: 'asha6428', email: 'astrid.haugen@dsv.su.se' }, + { id: 238, name: 'Lars Øvrebø', username: 'laøv4753', email: 'lars.ovrebo@dsv.su.se' }, + { id: 239, name: 'Ingrid Løvdal', username: 'inlø8596', email: 'ingrid.lovdal@dsv.su.se' }, + { id: 240, name: 'Erik Åsheim', username: 'erås2319', email: 'erik.asheim@dsv.su.se' }, + { id: 241, name: 'Kari Fjellheim', username: 'kafj7684', email: 'kari.fjellheim@dsv.su.se' }, + { id: 242, name: 'Olav Størdahl', username: 'olst5127', email: 'olav.stordahl@dsv.su.se' }, + { id: 243, name: 'Silje Rønning', username: 'sir8941', email: 'silje.ronning@dsv.su.se' }, + { id: 244, name: 'Torstein Bråten', username: 'tobr3465', email: 'torstein.braten@dsv.su.se' }, + { id: 245, name: 'Gunnhild Skjælaaen', username: 'gusk6782', email: 'gunnhild.skjaelaaen@dsv.su.se' }, + + // American names + { id: 246, name: 'Hunter McKenzie III', username: 'humc9218', email: 'hunter.mckenzie@dsv.su.se' }, + { id: 247, name: 'Madison Taylor-Brooks', username: 'mata4576', email: 'madison.taylor-brooks@dsv.su.se' }, + { id: 248, name: 'Braxton Washington Jr.', username: 'brwa7839', email: 'braxton.washington@dsv.su.se' }, + { id: 249, name: 'Skylar Kennedy-Davis', username: 'skke2153', email: 'skylar.kennedy-davis@dsv.su.se' }, + { id: 250, name: 'Preston Montgomery IV', username: 'prmo5694', email: 'preston.montgomery@dsv.su.se' } ]; export const USER = { diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index fd3f155..08770a4 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -101,8 +101,6 @@ export const SettingsProvider = ({ children }) => { // Get current user as participant object const getCurrentUser = () => { - console.log('Settings currentUserName:', settings.currentUserName); - console.log('USER.name:', USER.name); return { id: USER.id, name: settings.currentUserName, -- 2.39.5 From fa6e741fb4a31af30745ecfe18c2a78132a4c90d Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:14:45 +0200 Subject: [PATCH 22/41] new bookingcards, not finished --- my-app/src/AppRoutes.jsx | 56 ++++++++++++++- my-app/src/components/Booking.jsx | 31 -------- my-app/src/components/Booking.module.css | 44 ------------ my-app/src/components/BookingCard.jsx | 53 ++++++++++++++ my-app/src/components/BookingCard.module.css | 76 ++++++++++++++++++++ my-app/src/components/BookingsList.jsx | 4 +- 6 files changed, 186 insertions(+), 78 deletions(-) delete mode 100644 my-app/src/components/Booking.jsx delete mode 100644 my-app/src/components/Booking.module.css create mode 100644 my-app/src/components/BookingCard.jsx create mode 100644 my-app/src/components/BookingCard.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index db08a10..f1e71c7 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Routes, Route, useLocation } from 'react-router-dom'; import { useEffect, useState } from 'react'; +import { CalendarDate } from '@internationalized/date'; import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; @@ -10,7 +11,60 @@ import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { const location = useLocation(); const [loading, setLoading] = useState(false); - const [bookings, setBookings] = useState([]); + const [bookings, setBookings] = useState([ + { + id: 1, + date: new CalendarDate(2025, 9, 3), + startTime: 4, + endTime: 6, + room: 'G5:7', + title: 'Team standup', + participants: [ + { 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' } + ] + }, + { + id: 2, + date: new CalendarDate(2025, 9, 5), + startTime: 8, + endTime: 12, + room: 'G5:12', + title: 'Project planning workshop', + participants: [ + { 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' }, + { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' }, + { id: 9, name: 'Sofia Karlsson', username: 'soka1245', email: 'sofia.karlsson@dsv.su.se' }, + { id: 10, name: 'Magnus Nilsson', username: 'mani6789', email: 'magnus.nilsson@dsv.su.se' } + ] + }, + { + id: 3, + date: new CalendarDate(2025, 9, 4), + startTime: 2, + endTime: 3, + room: 'G5:3', + title: '1:1 with supervisor', + participants: [ + { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' } + ] + }, + { + id: 4, + date: new CalendarDate(2025, 9, 6), + startTime: 6, + endTime: 8, + room: 'G5:15', + title: 'Study group session', + participants: [ + { 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' } + ] + } + ]); function addBooking(newBooking) { setBookings([...bookings, newBooking]); diff --git a/my-app/src/components/Booking.jsx b/my-app/src/components/Booking.jsx deleted file mode 100644 index e88f286..0000000 --- a/my-app/src/components/Booking.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import styles from './Booking.module.css'; -import { convertDateObjectToString } from '../helpers'; - -function Booking({ booking, handleEditBooking }) { - 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 ( -
handleEditBooking(booking)}> -
-

{convertDateObjectToString(booking.date)}

-

{booking.title}

-
-
-

{booking.room}

-

{getTimeFromIndex(booking.startTime)} - {getTimeFromIndex(booking.endTime)}

-
-
- ); -} - -export default Booking; \ No newline at end of file diff --git a/my-app/src/components/Booking.module.css b/my-app/src/components/Booking.module.css deleted file mode 100644 index df33613..0000000 --- a/my-app/src/components/Booking.module.css +++ /dev/null @@ -1,44 +0,0 @@ -.container { - display: flex; - justify-content: space-between; - border: 1px solid #E5E5E5; - padding: 0.7rem; - width: 100%; - border-radius: 0.5rem; -} - -.left { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.right { - display: flex; - flex-direction: column; - align-items: end; -} - -.container p { - margin: 0; -} - -.container:hover { - cursor: pointer; -} - -.date { - text-transform: uppercase; - font-size: 0.8rem; -} - -.room { - font-weight: 600; - font-size: 0.8rem; - color: #5d5d5d; -} - -.time { - font-size: 1.2rem; -} \ No newline at end of file diff --git a/my-app/src/components/BookingCard.jsx b/my-app/src/components/BookingCard.jsx new file mode 100644 index 0000000..e673877 --- /dev/null +++ b/my-app/src/components/BookingCard.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import styles from './BookingCard.module.css'; +import { convertDateObjectToString } from '../helpers'; + +function BookingCard({ booking, onClick }) { + 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'}`; + } + + + function formatParticipants(participants) { + if (!participants || participants.length === 0) return null; + + 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 ( +
+
+
+ {convertDateObjectToString(booking.date)} +
+
+
+ {getTimeFromIndex(booking.startTime)} - {getTimeFromIndex(booking.endTime)} +
+ {booking.room} +
+
+
+

{booking.title}

+ {booking.participants && booking.participants.length > 0 && ( +

{formatParticipants(booking.participants)}

+ )} +
+
+ ); +} + +export default BookingCard; \ No newline at end of file diff --git a/my-app/src/components/BookingCard.module.css b/my-app/src/components/BookingCard.module.css new file mode 100644 index 0000000..80a2137 --- /dev/null +++ b/my-app/src/components/BookingCard.module.css @@ -0,0 +1,76 @@ +.card { + border: 1px solid #E5E5E5; + padding: 1rem; + width: 100%; + border-radius: 0.75rem; + background: #fff; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.card:hover { + cursor: pointer; + border-color: #007AFF; + box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15); + transform: translateY(-1px); +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.dateContainer { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.date { + text-transform: uppercase; + font-size: 0.75rem; + font-weight: 600; + color: #666; + letter-spacing: 0.5px; +} + +.room { + font-weight: 600; + font-size: 0.875rem; + color: #666; +} + +.time { + font-size: 1.125rem; + font-weight: 600; + color: #333; +} + +.body { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.title { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #333; + line-height: 1.3; +} + +.participants { + margin: 0; + font-size: 0.875rem; + color: #666; + display: flex; + align-items: center; +} + +.participants::before { + content: "👥"; + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index 883fb89..b5de969 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -1,6 +1,6 @@ import React from 'react'; import styles from './BookingsList.module.css'; -import Booking from './Booking'; +import BookingCard from './BookingCard'; function BookingsList({ bookings, handleEditBooking }) { @@ -11,7 +11,7 @@ function BookingsList({ bookings, handleEditBooking }) { {bookings.length > 0 ? ( <> {bookings.map((booking, index) => ( - + handleEditBooking(booking)} /> ))} ) : ( -- 2.39.5 From d103930e78fbf73e49529a108f77f9b3acfe9f37 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:59:56 +0200 Subject: [PATCH 23/41] booking card improvments --- my-app/src/AppRoutes.jsx | 8 +- my-app/src/components/BookingCard.jsx | 27 +++-- my-app/src/components/BookingCard.module.css | 102 ++++++++++------ my-app/src/components/BookingsList.jsx | 52 +++++++- my-app/src/components/BookingsList.module.css | 113 ++++++++++++++++++ my-app/src/context/SettingsContext.jsx | 3 + my-app/src/hooks/useBookingState.js | 24 +++- my-app/src/pages/BookingSettings.jsx | 25 ++++ my-app/src/pages/BookingSettings.module.css | 45 +++++++ my-app/src/pages/RoomBooking.jsx | 14 ++- 10 files changed, 357 insertions(+), 56 deletions(-) diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index f1e71c7..dda2e6e 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -11,6 +11,7 @@ import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { const location = useLocation(); const [loading, setLoading] = useState(false); + const [showSuccessBanner, setShowSuccessBanner] = useState(false); const [bookings, setBookings] = useState([ { id: 1, @@ -18,6 +19,7 @@ const AppRoutes = () => { startTime: 4, endTime: 6, room: 'G5:7', + roomCategory: 'green', title: 'Team standup', participants: [ { id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' }, @@ -31,6 +33,7 @@ const AppRoutes = () => { startTime: 8, endTime: 12, room: 'G5:12', + roomCategory: 'red', title: 'Project planning workshop', participants: [ { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' }, @@ -47,6 +50,7 @@ const AppRoutes = () => { startTime: 2, endTime: 3, room: 'G5:3', + roomCategory: 'blue', title: '1:1 with supervisor', participants: [ { id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' } @@ -58,6 +62,7 @@ const AppRoutes = () => { startTime: 6, endTime: 8, room: 'G5:15', + roomCategory: 'yellow', title: 'Study group session', participants: [ { id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' }, @@ -68,6 +73,7 @@ const AppRoutes = () => { function addBooking(newBooking) { setBookings([...bookings, newBooking]); + setShowSuccessBanner(true); } useEffect(() => { @@ -84,7 +90,7 @@ const AppRoutes = () => { }> - } /> + setShowSuccessBanner(false)} />} /> } /> } /> diff --git a/my-app/src/components/BookingCard.jsx b/my-app/src/components/BookingCard.jsx index e673877..1976b6e 100644 --- a/my-app/src/components/BookingCard.jsx +++ b/my-app/src/components/BookingCard.jsx @@ -12,6 +12,10 @@ function BookingCard({ booking, onClick }) { return `${hours}:${minutes === 0 ? '00' : '30'}`; } + + function getRoomCategoryClass(category) { + return `room-${category}`; + } function formatParticipants(participants) { @@ -30,21 +34,20 @@ function BookingCard({ booking, onClick }) { return (
-
+
{convertDateObjectToString(booking.date)} -
-
-
- {getTimeFromIndex(booking.startTime)} - {getTimeFromIndex(booking.endTime)} +
+

{booking.title}

+ {booking.room}
- {booking.room} + {booking.participants && booking.participants.length > 0 && ( +

{formatParticipants(booking.participants)}

+ )} +
+
+
{getTimeFromIndex(booking.startTime)}
+
{getTimeFromIndex(booking.endTime)}
-
-
-

{booking.title}

- {booking.participants && booking.participants.length > 0 && ( -

{formatParticipants(booking.participants)}

- )}
); diff --git a/my-app/src/components/BookingCard.module.css b/my-app/src/components/BookingCard.module.css index 80a2137..b4b1775 100644 --- a/my-app/src/components/BookingCard.module.css +++ b/my-app/src/components/BookingCard.module.css @@ -1,76 +1,110 @@ .card { border: 1px solid #E5E5E5; - padding: 1rem; + padding: 1.25rem; width: 100%; - border-radius: 0.75rem; + border-radius: 0.5rem; background: #fff; transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .card:hover { cursor: pointer; border-color: #007AFF; - box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15); - transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0, 122, 255, 0.12); + transform: translateY(-2px); } .header { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 0.75rem; + height: 100%; } -.dateContainer { - display: flex; - flex-direction: column; - gap: 0.25rem; +.leftSection { + flex: 1; } .date { text-transform: uppercase; - font-size: 0.75rem; + font-size: 0.8rem; font-weight: 600; - color: #666; - letter-spacing: 0.5px; + color: #999; + letter-spacing: 1px; + margin-bottom: 0.5rem; + display: block; +} + +.titleRow { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.title { + margin: 0; + font-size: 1.25rem; + font-weight: 700; + color: #333; + line-height: 1.3; } .room { font-weight: 600; font-size: 0.875rem; - color: #666; + padding: 0.375rem 0.75rem; + border-radius: 1rem; + white-space: nowrap; } -.time { - font-size: 1.125rem; - font-weight: 600; - color: #333; +.room-green { + background: #D4EDDA; + color: #155724; } -.body { +.room-red { + background: #F8D7DA; + color: #721C24; +} + +.room-blue { + background: #D1ECF1; + color: #0C5460; +} + +.room-yellow { + background: #FFF3CD; + color: #856404; +} + +.timeSection { display: flex; flex-direction: column; - gap: 0.5rem; + justify-content: center; + align-items: center; + /*background-color:rgba(0, 122, 255, 0.12);*/ + min-height: 80px; + gap: 0.2rem; } -.title { - margin: 0; - font-size: 1rem; - font-weight: 600; +.startTime { + font-size: 1.6rem; + font-weight: 400; color: #333; - line-height: 1.3; + line-height: 1; +} + +.endTime { + font-size: 1.6rem; + font-weight: 400; + color: #acacac; + line-height: 1; } .participants { margin: 0; - font-size: 0.875rem; - color: #666; - display: flex; - align-items: center; -} - -.participants::before { - content: "👥"; - margin-right: 0.5rem; + font-size: 0.9rem; + color: #999; } \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index b5de969..f985fb8 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -1,18 +1,62 @@ -import React from 'react'; +import React, { useState } from 'react'; import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; -function BookingsList({ bookings, handleEditBooking }) { +function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismissBanner, showMockDataBanner }) { + const [showAll, setShowAll] = useState(false); + const INITIAL_DISPLAY_COUNT = 3; + + const displayedBookings = showAll ? bookings : bookings.slice(0, INITIAL_DISPLAY_COUNT); + const hasMoreBookings = bookings.length > INITIAL_DISPLAY_COUNT; - return (
+ {showSuccessBanner && ( +
+
+ + Bokningen har skapats! +
+ +
+ )} + {showMockDataBanner && ( +
+
+ 🔧 + Visar testdata för utveckling +
+
+ )}
{bookings.length > 0 ? ( <> - {bookings.map((booking, index) => ( + {displayedBookings.map((booking, index) => ( handleEditBooking(booking)} /> ))} + {hasMoreBookings && ( + + )} ) : (

Du har inga bokningar just nu

diff --git a/my-app/src/components/BookingsList.module.css b/my-app/src/components/BookingsList.module.css index 2bd4ecd..2ea8401 100644 --- a/my-app/src/components/BookingsList.module.css +++ b/my-app/src/components/BookingsList.module.css @@ -22,4 +22,117 @@ .heading { margin: 0; margin-bottom: 0.5rem; +} + +.showMoreButton { + background: #ffffff; + border: none; + border-radius: 0.75rem; + padding: 1rem; + font-size: 1rem; + font-weight: 600; + color: rgb(0, 0, 0); + cursor: pointer; + transition: all 0.2s ease; + margin-top: 1rem; + width: 100%; + max-width: 100%; + box-shadow: 0 2px 8px rgba(143, 143, 143, 0.2); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + box-sizing: border-box; +} + +.showMoreButton:hover { + background: #f2f6ff; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(192, 192, 192, 0.3); +} + +.showMoreButton:active { + transform: translateY(0); +} + +.successBanner { + background: #F8FFF9; + border: 1px solid #28A745; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(40, 167, 69, 0.15); +} + +.bannerContent { + display: flex; + align-items: center; + gap: 1rem; +} + +.successIcon { + color: #28A745; + font-size: 1.25rem; + font-weight: bold; +} + +.bannerContent span:last-child { + color: #155724; + font-weight: 600; + font-size: 1rem; +} + +.bannerCloseButton { + background: none; + border: none; + color: #6C757D; + font-size: 1.5rem; + font-weight: 300; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + transition: all 0.2s ease; + line-height: 1; +} + +.bannerCloseButton:hover { + background: rgba(108, 117, 125, 0.1); + color: #495057; +} + +.mockDataBanner { + background: #FFF8E1; + border: 1px solid #FFB74D; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + box-shadow: 0 2px 8px rgba(255, 183, 77, 0.15); +} + +.mockIcon { + color: #F57F17; + font-size: 1.25rem; + margin-right: 1rem; +} + +.mockDataBanner .bannerContent span:last-child { + color: #E65100; + font-weight: 600; + font-size: 1rem; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 08770a4..5a4f26c 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -28,6 +28,7 @@ export const SettingsProvider = ({ children }) => { earliestTimeSlot: 0, latestTimeSlot: 23, currentUserName: USER.name, + showMockDataBanner: false, // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -55,6 +56,8 @@ export const SettingsProvider = ({ children }) => { latestTimeSlot: 23, // 19:30 (last slot ending at 20:00) // Current user settings currentUserName: USER.name, + // Mock data banner toggle + showMockDataBanner: false, }; }); diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index c4a8f42..2918ee9 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -6,10 +6,24 @@ import { generateId, findObjectById } from '../utils/bookingUtils'; -import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants'; +import { DEFAULT_BOOKING_TITLE, PEOPLE, USER } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; import { useSettingsContext } from '../context/SettingsContext'; +function getRoomCategory(roomName) { + // Extract room number from room name (e.g., "G5:7" -> 7) + const roomNumber = parseInt(roomName.split(':')[1]); + + // Assign categories based on room number ranges + if (roomNumber >= 1 && roomNumber <= 4) return 'green'; + if (roomNumber >= 5 && roomNumber <= 8) return 'red'; + if (roomNumber >= 9 && roomNumber <= 12) return 'blue'; + if (roomNumber >= 13 && roomNumber <= 15) return 'yellow'; + + // Default fallback + return 'green'; +} + export function useBookingState(addBooking, initialDate = null) { const { settings } = useSettingsContext(); @@ -100,14 +114,20 @@ export function useBookingState(addBooking, initialDate = null) { title }); + // Include the current user as a participant if not already added + const allParticipants = participants.find(p => p.id === USER.id) + ? participants + : [USER, ...participants]; + addBooking({ id: generateId(), date: selectedDate, startTime: selectedStartIndex, endTime: selectedEndIndex, room: selectedRoom, + roomCategory: getRoomCategory(selectedRoom), title: title !== "" ? title : DEFAULT_BOOKING_TITLE, - participants: participants + participants: allParticipants }); resetSelections(); diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 5ae5f12..47c7710 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -63,6 +63,31 @@ export function BookingSettings() {
+
+

Display Settings

+ +
+ +
+ updateSettings({ showMockDataBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showMockDataBanner ? 'Enabled' : 'Disabled'} + +
+
+
+

Date Settings

diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css index 8496859..6fe559e 100644 --- a/my-app/src/pages/BookingSettings.module.css +++ b/my-app/src/pages/BookingSettings.module.css @@ -186,6 +186,51 @@ text-align: center; } +.toggleGroup { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.toggle { + width: 50px; + height: 24px; + background: #d1d5db; + border-radius: 12px; + border: none; + position: relative; + cursor: pointer; + transition: background-color 0.3s ease; + -webkit-appearance: none; + appearance: none; +} + +.toggle:checked { + background: #10b981; +} + +.toggle:before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background: white; + top: 2px; + left: 2px; + transition: transform 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.toggle:checked:before { + transform: translateX(26px); +} + +.toggleStatus { + font-weight: 500; + color: #374151; +} + .actions { display: flex; flex-direction: column; diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 5732897..ab332a3 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -3,19 +3,27 @@ import styles from './RoomBooking.module.css'; import { Link } from 'react-router-dom'; import BookingsList from '../components/BookingsList'; import Card from '../components/Card'; +import { useSettingsContext } from '../context/SettingsContext'; -export function RoomBooking({ bookings }) { +export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) { + const { settings } = useSettingsContext(); function handleEditBooking(booking) { console.log(booking); - setIsEditBooking(booking); + // setIsEditBooking(booking); // This line seems to have an error, commenting out } return (

Lokalbokning

Mina bokingar

- +

Ny bokning

-- 2.39.5 From 3e327b291759db4d809d2836ed4e027dd3663c31 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:40:54 +0200 Subject: [PATCH 24/41] booking banner --- my-app/src/AppRoutes.jsx | 4 +- .../components/BookingConfirmationBanner.jsx | 73 ++++++++++ .../BookingConfirmationBanner.module.css | 132 ++++++++++++++++++ my-app/src/components/BookingsList.jsx | 40 +++--- my-app/src/components/BookingsList.module.css | 48 +------ my-app/src/context/SettingsContext.jsx | 9 +- my-app/src/pages/BookingSettings.jsx | 35 ++++- my-app/src/pages/RoomBooking.jsx | 6 +- 8 files changed, 274 insertions(+), 73 deletions(-) create mode 100644 my-app/src/components/BookingConfirmationBanner.jsx create mode 100644 my-app/src/components/BookingConfirmationBanner.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index dda2e6e..dbcb5ed 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -12,6 +12,7 @@ const AppRoutes = () => { const location = useLocation(); const [loading, setLoading] = useState(false); const [showSuccessBanner, setShowSuccessBanner] = useState(false); + const [lastCreatedBooking, setLastCreatedBooking] = useState(null); const [bookings, setBookings] = useState([ { id: 1, @@ -73,6 +74,7 @@ const AppRoutes = () => { function addBooking(newBooking) { setBookings([...bookings, newBooking]); + setLastCreatedBooking(newBooking); setShowSuccessBanner(true); } @@ -90,7 +92,7 @@ const AppRoutes = () => { }> - setShowSuccessBanner(false)} />} /> + setShowSuccessBanner(false)} />} /> } /> } /> diff --git a/my-app/src/components/BookingConfirmationBanner.jsx b/my-app/src/components/BookingConfirmationBanner.jsx new file mode 100644 index 0000000..d0adb4a --- /dev/null +++ b/my-app/src/components/BookingConfirmationBanner.jsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import styles from './BookingConfirmationBanner.module.css'; +import { convertDateObjectToString } from '../helpers'; + +function BookingConfirmationBanner({ booking, onClose, showCloseButton = false, showFakeCloseButton = false, isTestBanner = false }) { + const [showTooltip, setShowTooltip] = useState(false); + 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'}`; + } + + function formatBookingDetails(booking) { + if (!booking) return ''; + + const dateStr = convertDateObjectToString(booking.date); + const startTime = getTimeFromIndex(booking.startTime); + const endTime = getTimeFromIndex(booking.endTime); + + return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`; + } + + if (!booking) return null; + + const handleFakeClose = () => { + setShowTooltip(true); + setTimeout(() => setShowTooltip(false), 3000); + }; + + return ( +
+
+ +
+
+ Bokning bekräftad {booking.title && {booking.title}} + {isTestBanner && TEST} +
+ {formatBookingDetails(booking)} +
+
+ {showCloseButton && ( + + )} + {showFakeCloseButton && ( +
+ + {showTooltip && ( +
+ Detta är en testbanner som inte kan stängas +
+ )} +
+ )} +
+ ); +} + +export default BookingConfirmationBanner; \ No newline at end of file diff --git a/my-app/src/components/BookingConfirmationBanner.module.css b/my-app/src/components/BookingConfirmationBanner.module.css new file mode 100644 index 0000000..e701bcd --- /dev/null +++ b/my-app/src/components/BookingConfirmationBanner.module.css @@ -0,0 +1,132 @@ +.confirmationBanner { + background: #E8F5E8; + border: 1px solid #4CAF50; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15); +} + +.bannerContent { + display: flex; + align-items: center; + gap: 1rem; +} + +.confirmationIcon { + background: #4CAF50; + color: white; + width: 2rem; + height: 2rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1rem; + flex-shrink: 0; +} + +.confirmationText { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.titleRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.confirmationTitle { + color: #2E7D32; + font-weight: 700; + font-size: 1.1rem; +} + +.testLabel { + background: #FF9800; + color: white; + font-size: 0.7rem; + font-weight: 700; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.confirmationDetails { + color: #388E3C; + font-weight: 500; + font-size: 0.9rem; +} + +.bannerCloseButton { + background: none; + border: none; + color: #6C757D; + font-size: 1.5rem; + font-weight: 300; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + transition: all 0.2s ease; + line-height: 1; + flex-shrink: 0; + width: fit-content; +} + +.bannerCloseButton:hover { + background: rgba(108, 117, 125, 0.1); + color: #495057; +} + +.fakeCloseContainer { + position: relative; +} + +.tooltip { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.5rem; + background: #333; + color: white; + padding: 0.75rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + white-space: nowrap; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + animation: fadeIn 0.2s ease-out; +} + +.tooltip::before { + content: ''; + position: absolute; + bottom: 100%; + right: 1rem; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #333; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.bookingTitle { + font-weight: 400; + margin-left: 0.5rem; +} \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index f985fb8..c9f5f69 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -1,8 +1,10 @@ import React, { useState } from 'react'; +import { CalendarDate } from '@internationalized/date'; import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; +import BookingConfirmationBanner from './BookingConfirmationBanner'; -function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismissBanner, showMockDataBanner }) { +function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) { const [showAll, setShowAll] = useState(false); const INITIAL_DISPLAY_COUNT = 3; @@ -12,27 +14,33 @@ function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismis return (
{showSuccessBanner && ( -
-
- - Bokningen har skapats! -
- -
+ )} - {showMockDataBanner && ( -
+ {showDevelopmentBanner && ( +
- 🔧 + 🔧 Visar testdata för utveckling
)} + {showBookingConfirmationBanner && ( + + )}
{bookings.length > 0 ? ( <> diff --git a/my-app/src/components/BookingsList.module.css b/my-app/src/components/BookingsList.module.css index 2ea8401..cb596b9 100644 --- a/my-app/src/components/BookingsList.module.css +++ b/my-app/src/components/BookingsList.module.css @@ -55,55 +55,14 @@ transform: translateY(0); } -.successBanner { - background: #F8FFF9; - border: 1px solid #28A745; - border-radius: 0.75rem; - padding: 1rem 1.25rem; - margin-bottom: 1.5rem; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 2px 8px rgba(40, 167, 69, 0.15); -} - .bannerContent { display: flex; align-items: center; gap: 1rem; } -.successIcon { - color: #28A745; - font-size: 1.25rem; - font-weight: bold; -} -.bannerContent span:last-child { - color: #155724; - font-weight: 600; - font-size: 1rem; -} - -.bannerCloseButton { - background: none; - border: none; - color: #6C757D; - font-size: 1.5rem; - font-weight: 300; - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: 0.375rem; - transition: all 0.2s ease; - line-height: 1; -} - -.bannerCloseButton:hover { - background: rgba(108, 117, 125, 0.1); - color: #495057; -} - -.mockDataBanner { +.developmentBanner { background: #FFF8E1; border: 1px solid #FFB74D; border-radius: 0.75rem; @@ -114,18 +73,19 @@ box-shadow: 0 2px 8px rgba(255, 183, 77, 0.15); } -.mockIcon { +.developmentIcon { color: #F57F17; font-size: 1.25rem; margin-right: 1rem; } -.mockDataBanner .bannerContent span:last-child { +.developmentBanner .bannerContent span:last-child { color: #E65100; font-weight: 600; font-size: 1rem; } + @keyframes slideDown { from { opacity: 0; diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 5a4f26c..1a77279 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -28,7 +28,8 @@ export const SettingsProvider = ({ children }) => { earliestTimeSlot: 0, latestTimeSlot: 23, currentUserName: USER.name, - showMockDataBanner: false, + showDevelopmentBanner: false, + showBookingConfirmationBanner: false, // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -56,8 +57,10 @@ export const SettingsProvider = ({ children }) => { latestTimeSlot: 23, // 19:30 (last slot ending at 20:00) // Current user settings currentUserName: USER.name, - // Mock data banner toggle - showMockDataBanner: false, + // Development banner toggle + showDevelopmentBanner: false, + // Booking confirmation banner toggle + showBookingConfirmationBanner: false, }; }); diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 47c7710..56f211d 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -67,22 +67,43 @@ export function BookingSettings() {

Display Settings

-
+ +
+ +
+ updateSettings({ showBookingConfirmationBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showBookingConfirmationBanner ? 'Enabled' : 'Disabled'}
diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index ab332a3..5bf2bb8 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -5,7 +5,7 @@ import BookingsList from '../components/BookingsList'; import Card from '../components/Card'; import { useSettingsContext } from '../context/SettingsContext'; -export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) { +export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner }) { const { settings } = useSettingsContext(); function handleEditBooking(booking) { @@ -21,8 +21,10 @@ export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) { bookings={bookings} handleEditBooking={handleEditBooking} showSuccessBanner={showSuccessBanner} + lastCreatedBooking={lastCreatedBooking} onDismissBanner={onDismissBanner} - showMockDataBanner={settings.showMockDataBanner} + showDevelopmentBanner={settings.showDevelopmentBanner} + showBookingConfirmationBanner={settings.showBookingConfirmationBanner} />

Ny bokning

-- 2.39.5 From 8019ac2145cc7944db2fefb095d73be51b12a9f4 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:12:02 +0200 Subject: [PATCH 25/41] Test Gitea workflow with small comment addition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- my-app/src/components/BookingConfirmationBanner.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/my-app/src/components/BookingConfirmationBanner.jsx b/my-app/src/components/BookingConfirmationBanner.jsx index d0adb4a..67b236b 100644 --- a/my-app/src/components/BookingConfirmationBanner.jsx +++ b/my-app/src/components/BookingConfirmationBanner.jsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import styles from './BookingConfirmationBanner.module.css'; import { convertDateObjectToString } from '../helpers'; +// Test change for Gitea workflow verification + function BookingConfirmationBanner({ booking, onClose, showCloseButton = false, showFakeCloseButton = false, isTestBanner = false }) { const [showTooltip, setShowTooltip] = useState(false); function getTimeFromIndex(timeIndex) { -- 2.39.5 From 754a4c0234f19ded46d2467f6e676ccd3d9e9e43 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:15:20 +0200 Subject: [PATCH 26/41] Update BookingConfirmationBanner.jsx --- my-app/src/components/BookingConfirmationBanner.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/my-app/src/components/BookingConfirmationBanner.jsx b/my-app/src/components/BookingConfirmationBanner.jsx index 67b236b..b63b4cb 100644 --- a/my-app/src/components/BookingConfirmationBanner.jsx +++ b/my-app/src/components/BookingConfirmationBanner.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import styles from './BookingConfirmationBanner.module.css'; import { convertDateObjectToString } from '../helpers'; -// Test change for Gitea workflow verification +// Test change again function BookingConfirmationBanner({ booking, onClose, showCloseButton = false, showFakeCloseButton = false, isTestBanner = false }) { const [showTooltip, setShowTooltip] = useState(false); -- 2.39.5 From 8c80f5faec6396d838e0413320ffd9f7a6a873dd Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:19:44 +0200 Subject: [PATCH 27/41] confirmation banner small stuff --- my-app/src/components/BookingConfirmationBanner.jsx | 2 +- my-app/src/components/BookingConfirmationBanner.module.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/my-app/src/components/BookingConfirmationBanner.jsx b/my-app/src/components/BookingConfirmationBanner.jsx index b63b4cb..06c1c91 100644 --- a/my-app/src/components/BookingConfirmationBanner.jsx +++ b/my-app/src/components/BookingConfirmationBanner.jsx @@ -39,7 +39,7 @@ function BookingConfirmationBanner({ booking, onClose, showCloseButton = false,
- Bokning bekräftad {booking.title && {booking.title}} + Bokning bekräftad: {booking.title && {booking.title}} {isTestBanner && TEST}
{formatBookingDetails(booking)} diff --git a/my-app/src/components/BookingConfirmationBanner.module.css b/my-app/src/components/BookingConfirmationBanner.module.css index e701bcd..0243e61 100644 --- a/my-app/src/components/BookingConfirmationBanner.module.css +++ b/my-app/src/components/BookingConfirmationBanner.module.css @@ -128,5 +128,4 @@ .bookingTitle { font-weight: 400; - margin-left: 0.5rem; } \ No newline at end of file -- 2.39.5 From 5750c1bca36bba3f20759ccd97f3fea2a7a13b24 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:22:22 +0200 Subject: [PATCH 28/41] remove padding --- my-app/src/components/BookingsList.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/my-app/src/components/BookingsList.module.css b/my-app/src/components/BookingsList.module.css index cb596b9..bf97f4a 100644 --- a/my-app/src/components/BookingsList.module.css +++ b/my-app/src/components/BookingsList.module.css @@ -1,5 +1,4 @@ .bookingsListContainer { - padding: 1rem; /*border-top: 1px solid gray;*/ padding-bottom: 2rem; display: flex; -- 2.39.5 From 8eed906189850db122779dc8b8a4b460c1a58ebb Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:30:07 +0200 Subject: [PATCH 29/41] Handle booking modal --- my-app/src/AppRoutes.jsx | 8 +- my-app/src/components/BookingDetailsModal.jsx | 181 +++++++++++++ .../components/BookingDetailsModal.module.css | 244 ++++++++++++++++++ my-app/src/components/BookingsList.jsx | 23 +- my-app/src/components/Dropdown.module.css | 12 +- my-app/src/pages/RoomBooking.jsx | 3 +- 6 files changed, 463 insertions(+), 8 deletions(-) create mode 100644 my-app/src/components/BookingDetailsModal.jsx create mode 100644 my-app/src/components/BookingDetailsModal.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index dbcb5ed..c730c95 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -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 = () => { }> - setShowSuccessBanner(false)} />} /> + setShowSuccessBanner(false)} onBookingUpdate={updateBooking} />} /> } /> } /> diff --git a/my-app/src/components/BookingDetailsModal.jsx b/my-app/src/components/BookingDetailsModal.jsx new file mode 100644 index 0000000..c2c68a0 --- /dev/null +++ b/my-app/src/components/BookingDetailsModal.jsx @@ -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 ( + + +
+ + {booking.title} + + +
+ +
+
+ +

{convertDateObjectToString(booking.date)}

+
+ +
+
+
+ + + {getTimeFromIndex(booking.startTime)} + +
+
+
+ + + {getTimeFromIndex(calculatedEndTime || booking.endTime)} + +
+
+
+ +
+ + +
+ +
+ +

{booking.room}

+
+ +
+ +

{formatParticipants(booking.participants)}

+
+
+ +
+ + +
+
+
+ ); +} + +export default BookingDetailsModal; \ No newline at end of file diff --git a/my-app/src/components/BookingDetailsModal.module.css b/my-app/src/components/BookingDetailsModal.module.css new file mode 100644 index 0000000..e1f2ec5 --- /dev/null +++ b/my-app/src/components/BookingDetailsModal.module.css @@ -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; +} \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index c9f5f69..ec799f6 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -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 (
{showSuccessBanner && ( @@ -45,7 +58,7 @@ function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCrea {bookings.length > 0 ? ( <> {displayedBookings.map((booking, index) => ( - handleEditBooking(booking)} /> + handleBookingClick(booking)} /> ))} {hasMoreBookings && (
+
); } diff --git a/my-app/src/components/Dropdown.module.css b/my-app/src/components/Dropdown.module.css index d681482..4b44050 100644 --- a/my-app/src/components/Dropdown.module.css +++ b/my-app/src/components/Dropdown.module.css @@ -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; } diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 5bf2bb8..6d68bc0 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -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 Date: Wed, 3 Sep 2025 13:58:23 +0200 Subject: [PATCH 30/41] Handle booking expand instead of modal --- my-app/src/components/BookingCard.jsx | 154 ++++++++++- my-app/src/components/BookingCard.module.css | 248 ++++++++++++++++++ my-app/src/components/BookingTitleField.jsx | 6 +- .../components/BookingTitleField.module.css | 30 +++ my-app/src/components/BookingsList.jsx | 26 +- my-app/src/components/Dropdown.jsx | 2 +- .../src/components/ParticipantsSelector.jsx | 8 +- .../ParticipantsSelector.module.css | 34 +++ 8 files changed, 478 insertions(+), 30 deletions(-) diff --git a/my-app/src/components/BookingCard.jsx b/my-app/src/components/BookingCard.jsx index 1976b6e..680f54d 100644 --- a/my-app/src/components/BookingCard.jsx +++ b/my-app/src/components/BookingCard.jsx @@ -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 ( -
-
+
+
{convertDateObjectToString(booking.date)}
@@ -46,9 +146,53 @@ function BookingCard({ booking, onClick }) {
{getTimeFromIndex(booking.startTime)}
-
{getTimeFromIndex(booking.endTime)}
+
{getTimeFromIndex(calculatedEndTime || booking.endTime)}
+ + {isExpanded && ( + +
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+ )}
); } diff --git a/my-app/src/components/BookingCard.module.css b/my-app/src/components/BookingCard.module.css index b4b1775..a3b70bf 100644 --- a/my-app/src/components/BookingCard.module.css +++ b/my-app/src/components/BookingCard.module.css @@ -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; } \ No newline at end of file diff --git a/my-app/src/components/BookingTitleField.jsx b/my-app/src/components/BookingTitleField.jsx index 343cc4e..cd02fcd 100644 --- a/my-app/src/components/BookingTitleField.jsx +++ b/my-app/src/components/BookingTitleField.jsx @@ -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 ( <> -

Titel på bokning

+

Titel på bokning

booking.setTitle(event.target.value)} placeholder={DEFAULT_BOOKING_TITLE} - className={styles.textInput} + className={compact ? styles.compactTextInput : styles.textInput} /> ); diff --git a/my-app/src/components/BookingTitleField.module.css b/my-app/src/components/BookingTitleField.module.css index 4040478..b6963db 100644 --- a/my-app/src/components/BookingTitleField.module.css +++ b/my-app/src/components/BookingTitleField.module.css @@ -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; } \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index ec799f6..848342a 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -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) => ( - handleBookingClick(booking)} /> + handleBookingClick(booking)} + isExpanded={expandedBookingId === booking.id} + onBookingUpdate={onBookingUpdate} + /> ))} {hasMoreBookings && (
-
); } diff --git a/my-app/src/components/Dropdown.jsx b/my-app/src/components/Dropdown.jsx index 5807b2f..b03224a 100644 --- a/my-app/src/components/Dropdown.jsx +++ b/my-app/src/components/Dropdown.jsx @@ -12,7 +12,7 @@ const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: " className={styles.select} > {placeholder && ( - )} diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index 61d7ea5..ac061fe 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -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 ( -
-

Deltagare

+
+

Deltagare

{/* Search Input */}
@@ -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" diff --git a/my-app/src/components/ParticipantsSelector.module.css b/my-app/src/components/ParticipantsSelector.module.css index cbbe0d4..83a0828 100644 --- a/my-app/src/components/ParticipantsSelector.module.css +++ b/my-app/src/components/ParticipantsSelector.module.css @@ -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; } \ No newline at end of file -- 2.39.5 From b5e9c738dd4fc787ce11223f45ff5116a17d8fc7 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:31:33 +0200 Subject: [PATCH 31/41] delete booking works --- my-app/src/AppRoutes.jsx | 6 +- my-app/src/components/BookingCard.jsx | 80 +++++++++--- my-app/src/components/BookingCard.module.css | 124 ++++++++++++++++++- my-app/src/components/BookingsList.jsx | 3 +- my-app/src/pages/RoomBooking.jsx | 3 +- 5 files changed, 196 insertions(+), 20 deletions(-) diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index c730c95..ac2471b 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -84,6 +84,10 @@ const AppRoutes = () => { )); } + function deleteBooking(bookingToDelete) { + setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); + } + useEffect(() => { setLoading(true); const timer = setTimeout(() => setLoading(false), 800); @@ -98,7 +102,7 @@ const AppRoutes = () => { }> - setShowSuccessBanner(false)} onBookingUpdate={updateBooking} />} /> + setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} />} /> } /> } /> diff --git a/my-app/src/components/BookingCard.jsx b/my-app/src/components/BookingCard.jsx index 680f54d..f065ae8 100644 --- a/my-app/src/components/BookingCard.jsx +++ b/my-app/src/components/BookingCard.jsx @@ -8,11 +8,12 @@ import { ParticipantsSelector } from './ParticipantsSelector'; import { BookingProvider } from '../context/BookingContext'; import { PEOPLE } from '../constants/bookingConstants'; -function BookingCard({ booking, onClick, isExpanded, onBookingUpdate }) { +function BookingCard({ booking, onClick, isExpanded, onBookingUpdate, onBookingDelete }) { const [selectedLength, setSelectedLength] = useState(null); const [calculatedEndTime, setCalculatedEndTime] = useState(null); const [editedTitle, setEditedTitle] = useState(''); const [editedParticipants, setEditedParticipants] = useState([]); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // Calculate current booking length and available hours const currentLength = booking.endTime - booking.startTime; @@ -102,6 +103,21 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate }) { onClick(); // Close the expanded view } + function handleDelete() { + setShowDeleteConfirm(true); + } + + function confirmDelete() { + if (onBookingDelete) { + onBookingDelete(booking); + } + setShowDeleteConfirm(false); + } + + function cancelDelete() { + setShowDeleteConfirm(false); + } + function getTimeFromIndex(timeIndex) { const totalHalfHoursFromStart = timeIndex; @@ -175,21 +191,53 @@ function BookingCard({ booking, onClick, isExpanded, onBookingUpdate }) { />
-
- - -
+ {!showDeleteConfirm ? ( +
+ + + +
+ ) : ( +
+
+ ⚠️ +

Är du säker på att du vill radera denna bokning?

+

+ "{booking.title}" den {booking.date.day}/{booking.date.month} kl. {getTimeFromIndex(booking.startTime)} +

+
+
+ + +
+
+ )}
)} diff --git a/my-app/src/components/BookingCard.module.css b/my-app/src/components/BookingCard.module.css index a3b70bf..eacd6b3 100644 --- a/my-app/src/components/BookingCard.module.css +++ b/my-app/src/components/BookingCard.module.css @@ -153,6 +153,31 @@ align-items: center; } +.deleteButton { + flex: 2; + background-color: #DC2626; + color: white; + height: 3rem; + font-weight: 600; + font-size: 0.9rem; + border: 2px solid #B91C1C; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2); + cursor: pointer; +} + +.deleteButton:hover { + background-color: #B91C1C; + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +.deleteButton:active { + background-color: #991B1B; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(220, 38, 38, 0.2); +} + .cancelButton { flex: 2; background-color: white; @@ -202,7 +227,8 @@ } .saveButton[data-focused], -.cancelButton[data-focused] { +.cancelButton[data-focused], +.deleteButton[data-focused] { outline: 2px solid #2563EB; outline-offset: -1px; } @@ -355,4 +381,100 @@ color: #999; font-style: italic; font-size: 0.9rem; +} + +/* Confirmation dialog styles */ +.confirmationSection { + background-color: #FFF8DC; + border: 2px solid #FFD700; + border-radius: 0.5rem; + padding: 1.5rem; + margin-top: 1rem; +} + +.confirmationMessage { + text-align: center; + margin-bottom: 1.5rem; +} + +.warningIcon { + font-size: 2rem; + display: block; + margin-bottom: 0.5rem; +} + +.confirmationMessage p { + margin: 0.5rem 0; + color: #333; +} + +.confirmationMessage p:first-of-type { + font-weight: 600; + font-size: 1.1rem; +} + +.bookingDetails { + font-size: 0.9rem; + color: #666; + font-style: italic; +} + +.confirmationButtons { + display: flex; + gap: 1rem; + justify-content: center; +} + +.confirmDeleteButton { + background-color: #DC2626; + color: white; + height: 3rem; + font-weight: 600; + font-size: 0.95rem; + border: 2px solid #B91C1C; + border-radius: 0.5rem; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2); + cursor: pointer; + padding: 0 1.5rem; +} + +.confirmDeleteButton:hover { + background-color: #B91C1C; + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +.confirmDeleteButton:active { + background-color: #991B1B; + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(220, 38, 38, 0.2); +} + +.cancelDeleteButton { + 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.95rem; + padding: 0 1.5rem; +} + +.cancelDeleteButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelDeleteButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.confirmDeleteButton[data-focused], +.cancelDeleteButton[data-focused] { + outline: 2px solid #2563EB; + outline-offset: -1px; } \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index 848342a..f8f218c 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -4,7 +4,7 @@ import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; import BookingConfirmationBanner from './BookingConfirmationBanner'; -function BookingsList({ bookings, handleEditBooking, onBookingUpdate, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) { +function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) { const [showAll, setShowAll] = useState(false); const [expandedBookingId, setExpandedBookingId] = useState(null); const INITIAL_DISPLAY_COUNT = 3; @@ -56,6 +56,7 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, showSucces onClick={() => handleBookingClick(booking)} isExpanded={expandedBookingId === booking.id} onBookingUpdate={onBookingUpdate} + onBookingDelete={onBookingDelete} /> ))} {hasMoreBookings && ( diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 6d68bc0..82e78d6 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -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, onBookingUpdate }) { +export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete }) { const { settings } = useSettingsContext(); function handleEditBooking(booking) { @@ -21,6 +21,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o bookings={bookings} handleEditBooking={handleEditBooking} onBookingUpdate={onBookingUpdate} + onBookingDelete={onBookingDelete} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={onDismissBanner} -- 2.39.5 From bdadd66f211f475227aeff4b13f8dc04e762371d Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:38:17 +0200 Subject: [PATCH 32/41] booking deleted banner --- my-app/src/AppRoutes.jsx | 6 +- my-app/src/components/BookingDeleteBanner.jsx | 73 ++++++++++ .../components/BookingDeleteBanner.module.css | 131 ++++++++++++++++++ my-app/src/components/BookingsList.jsx | 22 ++- my-app/src/context/SettingsContext.jsx | 3 + my-app/src/pages/BookingSettings.jsx | 21 +++ my-app/src/pages/RoomBooking.jsx | 6 +- 7 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 my-app/src/components/BookingDeleteBanner.jsx create mode 100644 my-app/src/components/BookingDeleteBanner.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index ac2471b..86fcff2 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -13,6 +13,8 @@ const AppRoutes = () => { const [loading, setLoading] = useState(false); const [showSuccessBanner, setShowSuccessBanner] = useState(false); const [lastCreatedBooking, setLastCreatedBooking] = useState(null); + const [showDeleteBanner, setShowDeleteBanner] = useState(false); + const [lastDeletedBooking, setLastDeletedBooking] = useState(null); const [bookings, setBookings] = useState([ { id: 1, @@ -86,6 +88,8 @@ const AppRoutes = () => { function deleteBooking(bookingToDelete) { setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id)); + setLastDeletedBooking(bookingToDelete); + setShowDeleteBanner(true); } useEffect(() => { @@ -102,7 +106,7 @@ const AppRoutes = () => { }> - setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} />} /> + setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} /> } /> } /> diff --git a/my-app/src/components/BookingDeleteBanner.jsx b/my-app/src/components/BookingDeleteBanner.jsx new file mode 100644 index 0000000..3a73464 --- /dev/null +++ b/my-app/src/components/BookingDeleteBanner.jsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import styles from './BookingDeleteBanner.module.css'; +import { convertDateObjectToString } from '../helpers'; + +function BookingDeleteBanner({ booking, onClose, showFakeCloseButton = false, isTestBanner = false }) { + const [showTooltip, setShowTooltip] = useState(false); + 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'}`; + } + + function formatBookingDetails(booking) { + if (!booking) return ''; + + const dateStr = convertDateObjectToString(booking.date); + const startTime = getTimeFromIndex(booking.startTime); + const endTime = getTimeFromIndex(booking.endTime); + + return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`; + } + + if (!booking) return null; + + const handleFakeClose = () => { + setShowTooltip(true); + setTimeout(() => setShowTooltip(false), 3000); + }; + + return ( +
+
+ 🗑️ +
+
+ Bokning raderad: {booking.title && {booking.title}} + {isTestBanner && TEST} +
+ {formatBookingDetails(booking)} +
+
+ {!showFakeCloseButton && ( + + )} + {showFakeCloseButton && ( +
+ + {showTooltip && ( +
+ Detta är en testbanner som inte kan stängas +
+ )} +
+ )} +
+ ); +} + +export default BookingDeleteBanner; \ No newline at end of file diff --git a/my-app/src/components/BookingDeleteBanner.module.css b/my-app/src/components/BookingDeleteBanner.module.css new file mode 100644 index 0000000..568d3ef --- /dev/null +++ b/my-app/src/components/BookingDeleteBanner.module.css @@ -0,0 +1,131 @@ +.deleteBanner { + background: #FFF4F4; + border: 1px solid #F87171; + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(248, 113, 113, 0.15); +} + +.bannerContent { + display: flex; + align-items: center; + gap: 1rem; +} + +.deleteIcon { + background: #EF4444; + color: white; + width: 2rem; + height: 2rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1rem; + flex-shrink: 0; +} + +.deleteText { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.titleRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.deleteTitle { + color: #DC2626; + font-weight: 700; + font-size: 1.1rem; +} + +.deleteDetails { + color: #EF4444; + font-weight: 500; + font-size: 0.9rem; +} + +.bannerCloseButton { + background: none; + border: none; + color: #6C757D; + font-size: 1.5rem; + font-weight: 300; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + transition: all 0.2s ease; + line-height: 1; + flex-shrink: 0; + width: fit-content; +} + +.bannerCloseButton:hover { + background: rgba(108, 117, 125, 0.1); + color: #495057; +} + +.bookingTitle { + font-weight: 400; +} + +.testLabel { + background: #FF9800; + color: white; + font-size: 0.7rem; + font-weight: 700; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.fakeCloseContainer { + position: relative; +} + +.tooltip { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.5rem; + background: #333; + color: white; + padding: 0.75rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + white-space: nowrap; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + animation: fadeIn 0.2s ease-out; +} + +.tooltip::before { + content: ''; + position: absolute; + bottom: 100%; + right: 1rem; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #333; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index f8f218c..1370979 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -3,8 +3,9 @@ import { CalendarDate } from '@internationalized/date'; import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; import BookingConfirmationBanner from './BookingConfirmationBanner'; +import BookingDeleteBanner from './BookingDeleteBanner'; -function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) { +function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) { const [showAll, setShowAll] = useState(false); const [expandedBookingId, setExpandedBookingId] = useState(null); const INITIAL_DISPLAY_COUNT = 3; @@ -25,6 +26,12 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD showCloseButton={true} /> )} + {showDeleteBanner && ( + + )} {showDevelopmentBanner && (
@@ -46,6 +53,19 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD isTestBanner={true} /> )} + {showBookingDeleteBanner && ( + + )}
{bookings.length > 0 ? ( <> diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 1a77279..d124d19 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -30,6 +30,7 @@ export const SettingsProvider = ({ children }) => { currentUserName: USER.name, showDevelopmentBanner: false, showBookingConfirmationBanner: false, + showBookingDeleteBanner: false, // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -61,6 +62,8 @@ export const SettingsProvider = ({ children }) => { showDevelopmentBanner: false, // Booking confirmation banner toggle showBookingConfirmationBanner: false, + // Booking delete banner toggle + showBookingDeleteBanner: false, }; }); diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 56f211d..8f6cfb7 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -107,6 +107,27 @@ export function BookingSettings() {
+ +
+ +
+ updateSettings({ showBookingDeleteBanner: e.target.checked })} + className={styles.toggle} + /> + + {settings.showBookingDeleteBanner ? 'Enabled' : 'Disabled'} + +
+
diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx index 82e78d6..4a18af8 100644 --- a/my-app/src/pages/RoomBooking.jsx +++ b/my-app/src/pages/RoomBooking.jsx @@ -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, onBookingUpdate, onBookingDelete }) { +export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) { const { settings } = useSettingsContext(); function handleEditBooking(booking) { @@ -25,8 +25,12 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={onDismissBanner} + showDeleteBanner={showDeleteBanner} + lastDeletedBooking={lastDeletedBooking} + onDismissDeleteBanner={onDismissDeleteBanner} showDevelopmentBanner={settings.showDevelopmentBanner} showBookingConfirmationBanner={settings.showBookingConfirmationBanner} + showBookingDeleteBanner={settings.showBookingDeleteBanner} />

Ny bokning

-- 2.39.5 From e71c14484e7daf2dda337d007f78d5351220e7e6 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:43:33 +0200 Subject: [PATCH 33/41] Combine banners into one component --- .../components/BookingConfirmationBanner.jsx | 75 ---------- .../BookingConfirmationBanner.module.css | 131 ------------------ my-app/src/components/BookingDeleteBanner.jsx | 73 ---------- my-app/src/components/BookingsList.jsx | 25 ++-- my-app/src/components/BookingsList.module.css | 29 ---- my-app/src/components/NotificationBanner.jsx | 129 +++++++++++++++++ ...dule.css => NotificationBanner.module.css} | 78 ++++++++--- 7 files changed, 204 insertions(+), 336 deletions(-) delete mode 100644 my-app/src/components/BookingConfirmationBanner.jsx delete mode 100644 my-app/src/components/BookingConfirmationBanner.module.css delete mode 100644 my-app/src/components/BookingDeleteBanner.jsx create mode 100644 my-app/src/components/NotificationBanner.jsx rename my-app/src/components/{BookingDeleteBanner.module.css => NotificationBanner.module.css} (75%) diff --git a/my-app/src/components/BookingConfirmationBanner.jsx b/my-app/src/components/BookingConfirmationBanner.jsx deleted file mode 100644 index 06c1c91..0000000 --- a/my-app/src/components/BookingConfirmationBanner.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from 'react'; -import styles from './BookingConfirmationBanner.module.css'; -import { convertDateObjectToString } from '../helpers'; - -// Test change again - -function BookingConfirmationBanner({ booking, onClose, showCloseButton = false, showFakeCloseButton = false, isTestBanner = false }) { - const [showTooltip, setShowTooltip] = useState(false); - 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'}`; - } - - function formatBookingDetails(booking) { - if (!booking) return ''; - - const dateStr = convertDateObjectToString(booking.date); - const startTime = getTimeFromIndex(booking.startTime); - const endTime = getTimeFromIndex(booking.endTime); - - return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`; - } - - if (!booking) return null; - - const handleFakeClose = () => { - setShowTooltip(true); - setTimeout(() => setShowTooltip(false), 3000); - }; - - return ( -
-
- -
-
- Bokning bekräftad: {booking.title && {booking.title}} - {isTestBanner && TEST} -
- {formatBookingDetails(booking)} -
-
- {showCloseButton && ( - - )} - {showFakeCloseButton && ( -
- - {showTooltip && ( -
- Detta är en testbanner som inte kan stängas -
- )} -
- )} -
- ); -} - -export default BookingConfirmationBanner; \ No newline at end of file diff --git a/my-app/src/components/BookingConfirmationBanner.module.css b/my-app/src/components/BookingConfirmationBanner.module.css deleted file mode 100644 index 0243e61..0000000 --- a/my-app/src/components/BookingConfirmationBanner.module.css +++ /dev/null @@ -1,131 +0,0 @@ -.confirmationBanner { - background: #E8F5E8; - border: 1px solid #4CAF50; - border-radius: 0.75rem; - padding: 1rem 1.25rem; - margin-bottom: 1.5rem; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15); -} - -.bannerContent { - display: flex; - align-items: center; - gap: 1rem; -} - -.confirmationIcon { - background: #4CAF50; - color: white; - width: 2rem; - height: 2rem; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - font-size: 1rem; - flex-shrink: 0; -} - -.confirmationText { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.titleRow { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.confirmationTitle { - color: #2E7D32; - font-weight: 700; - font-size: 1.1rem; -} - -.testLabel { - background: #FF9800; - color: white; - font-size: 0.7rem; - font-weight: 700; - padding: 0.2rem 0.4rem; - border-radius: 0.25rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.confirmationDetails { - color: #388E3C; - font-weight: 500; - font-size: 0.9rem; -} - -.bannerCloseButton { - background: none; - border: none; - color: #6C757D; - font-size: 1.5rem; - font-weight: 300; - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: 0.375rem; - transition: all 0.2s ease; - line-height: 1; - flex-shrink: 0; - width: fit-content; -} - -.bannerCloseButton:hover { - background: rgba(108, 117, 125, 0.1); - color: #495057; -} - -.fakeCloseContainer { - position: relative; -} - -.tooltip { - position: absolute; - top: 100%; - right: 0; - margin-top: 0.5rem; - background: #333; - color: white; - padding: 0.75rem 1rem; - border-radius: 0.375rem; - font-size: 0.875rem; - white-space: nowrap; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 10; - animation: fadeIn 0.2s ease-out; -} - -.tooltip::before { - content: ''; - position: absolute; - bottom: 100%; - right: 1rem; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #333; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.bookingTitle { - font-weight: 400; -} \ No newline at end of file diff --git a/my-app/src/components/BookingDeleteBanner.jsx b/my-app/src/components/BookingDeleteBanner.jsx deleted file mode 100644 index 3a73464..0000000 --- a/my-app/src/components/BookingDeleteBanner.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState } from 'react'; -import styles from './BookingDeleteBanner.module.css'; -import { convertDateObjectToString } from '../helpers'; - -function BookingDeleteBanner({ booking, onClose, showFakeCloseButton = false, isTestBanner = false }) { - const [showTooltip, setShowTooltip] = useState(false); - 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'}`; - } - - function formatBookingDetails(booking) { - if (!booking) return ''; - - const dateStr = convertDateObjectToString(booking.date); - const startTime = getTimeFromIndex(booking.startTime); - const endTime = getTimeFromIndex(booking.endTime); - - return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`; - } - - if (!booking) return null; - - const handleFakeClose = () => { - setShowTooltip(true); - setTimeout(() => setShowTooltip(false), 3000); - }; - - return ( -
-
- 🗑️ -
-
- Bokning raderad: {booking.title && {booking.title}} - {isTestBanner && TEST} -
- {formatBookingDetails(booking)} -
-
- {!showFakeCloseButton && ( - - )} - {showFakeCloseButton && ( -
- - {showTooltip && ( -
- Detta är en testbanner som inte kan stängas -
- )} -
- )} -
- ); -} - -export default BookingDeleteBanner; \ No newline at end of file diff --git a/my-app/src/components/BookingsList.jsx b/my-app/src/components/BookingsList.jsx index 1370979..91cc60e 100644 --- a/my-app/src/components/BookingsList.jsx +++ b/my-app/src/components/BookingsList.jsx @@ -2,8 +2,7 @@ import React, { useState } from 'react'; import { CalendarDate } from '@internationalized/date'; import styles from './BookingsList.module.css'; import BookingCard from './BookingCard'; -import BookingConfirmationBanner from './BookingConfirmationBanner'; -import BookingDeleteBanner from './BookingDeleteBanner'; +import NotificationBanner from './NotificationBanner'; function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) { const [showAll, setShowAll] = useState(false); @@ -20,28 +19,29 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD return (
{showSuccessBanner && ( - )} {showDeleteBanner && ( - )} {showDevelopmentBanner && ( -
-
- 🔧 - Visar testdata för utveckling -
-
+ )} {showBookingConfirmationBanner && ( - )} {showBookingDeleteBanner && ( - { + setShowTooltip(true); + setTimeout(() => setShowTooltip(false), 3000); + }; + + const config = BANNER_VARIANTS[variant] || BANNER_VARIANTS.success; + + // For development banner, use custom content + if (variant === 'development') { + return ( +
+
+ {config.icon} + {customTitle || config.title} +
+
+ ); + } + + // For booking-related banners (success/delete) + if (!booking && !customContent) return null; + + return ( +
+
+ + {config.icon} + +
+
+ + {customTitle || config.title} {booking && booking.title && {booking.title}} + + {isTestBanner && TEST} +
+ {booking && ( + + {formatBookingDetails(booking)} + + )} + {customContent && ( + + {customContent} + + )} +
+
+ {showCloseButton && ( + + )} + {showFakeCloseButton && ( +
+ + {showTooltip && ( +
+ Detta är en testbanner som inte kan stängas +
+ )} +
+ )} +
+ ); +} + +export default NotificationBanner; \ No newline at end of file diff --git a/my-app/src/components/BookingDeleteBanner.module.css b/my-app/src/components/NotificationBanner.module.css similarity index 75% rename from my-app/src/components/BookingDeleteBanner.module.css rename to my-app/src/components/NotificationBanner.module.css index 568d3ef..c60b351 100644 --- a/my-app/src/components/BookingDeleteBanner.module.css +++ b/my-app/src/components/NotificationBanner.module.css @@ -1,13 +1,12 @@ -.deleteBanner { - background: #FFF4F4; - border: 1px solid #F87171; +/* Base banner styles */ +.banner { border-radius: 0.75rem; padding: 1rem 1.25rem; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; - box-shadow: 0 2px 8px rgba(248, 113, 113, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .bannerContent { @@ -16,9 +15,7 @@ gap: 1rem; } -.deleteIcon { - background: #EF4444; - color: white; +.icon { width: 2rem; height: 2rem; border-radius: 50%; @@ -30,7 +27,7 @@ flex-shrink: 0; } -.deleteText { +.text { display: flex; flex-direction: column; gap: 0.25rem; @@ -42,19 +39,21 @@ gap: 0.5rem; } -.deleteTitle { - color: #DC2626; +.title { font-weight: 700; font-size: 1.1rem; } -.deleteDetails { - color: #EF4444; +.details { font-weight: 500; font-size: 0.9rem; } -.bannerCloseButton { +.bookingTitle { + font-weight: 400; +} + +.closeButton { background: none; border: none; color: #6C757D; @@ -69,15 +68,61 @@ width: fit-content; } -.bannerCloseButton:hover { +.closeButton:hover { background: rgba(108, 117, 125, 0.1); color: #495057; } -.bookingTitle { - font-weight: 400; +/* Success variant styles */ +.success { + background: #E8F5E8; + border: 1px solid #4CAF50; } +.successIcon { + background: #4CAF50; + color: white; +} + +.successTitle { + color: #2E7D32; +} + +.successDetails { + color: #388E3C; +} + +/* Delete variant styles */ +.delete { + background: #FFF4F4; + border: 1px solid #F87171; +} + +.deleteIcon { + background: #EF4444; + color: white; +} + +.deleteTitle { + color: #DC2626; +} + +.deleteDetails { + color: #EF4444; +} + +/* Development variant styles */ +.development { + background: #FFF8E1; + border: 1px solid #FFB74D; +} + +.developmentIcon { + font-size: 1.5rem; + color: #FF9800; +} + +/* Test label styles */ .testLabel { background: #FF9800; color: white; @@ -89,6 +134,7 @@ letter-spacing: 0.5px; } +/* Fake close and tooltip styles */ .fakeCloseContainer { position: relative; } -- 2.39.5 From 00937a0d5f2d2730452c16fff96175de101cb36e Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:53:02 +0200 Subject: [PATCH 34/41] booking page fixes --- my-app/src/components/BookingModal.jsx | 9 +++++---- my-app/src/components/Dropdown.jsx | 2 +- my-app/src/components/RoomSelectionField.jsx | 1 + my-app/src/components/TimeCard.jsx | 4 ++++ my-app/src/hooks/useBookingState.js | 19 ++++++++++++++----- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index c88b9f4..84fb2f8 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -11,7 +11,8 @@ export function BookingModal({ hoursAvailable, endTimeIndex, setEndTimeIndex, - className + className, + onClose }) { const booking = useBookingContext(); const { getCurrentUser } = useSettingsContext(); @@ -87,7 +88,7 @@ export function BookingModal({ return ( - + !isOpen && onClose && onClose()} className={className} style={{borderRadius: '0.4rem', overflow: 'hidden'}}> {booking.title == "" ? "Jacobs bokning" : booking.title} @@ -123,8 +124,8 @@ export function BookingModal({
- -

G5:12

+ +

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

diff --git a/my-app/src/components/Dropdown.jsx b/my-app/src/components/Dropdown.jsx index b03224a..5807b2f 100644 --- a/my-app/src/components/Dropdown.jsx +++ b/my-app/src/components/Dropdown.jsx @@ -12,7 +12,7 @@ const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: " className={styles.select} > {placeholder && ( - )} diff --git a/my-app/src/components/RoomSelectionField.jsx b/my-app/src/components/RoomSelectionField.jsx index 1474dc4..7174dc4 100644 --- a/my-app/src/components/RoomSelectionField.jsx +++ b/my-app/src/components/RoomSelectionField.jsx @@ -22,6 +22,7 @@ export function RoomSelectionField() {

Rum

booking.handleRoomChange(e)} placeholder={{ label: "Alla rum", diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index 21ae885..145c86d 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -77,6 +77,10 @@ export default function TimeCard({ endTimeIndex={endTimeIndex} setEndTimeIndex={setEndTimeIndex} className={styles.modalContainer} + onClose={() => { + // Reset time selections when modal is closed without booking + booking.resetTimeSelections(); + }} /> ); diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index 2918ee9..44245b7 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -38,6 +38,7 @@ export function useBookingState(addBooking, initialDate = null) { ); const [currentRoom, setCurrentRoom] = useState(null); const [selectedRoom, setSelectedRoom] = useState("allRooms"); + const [assignedRoom, setAssignedRoom] = useState(null); const [selectedStartIndex, setSelectedStartIndex] = useState(null); const [selectedEndIndex, setSelectedEndIndex] = useState(null); const [selectedBookingLength, setSelectedBookingLength] = useState(0); @@ -59,6 +60,7 @@ export function useBookingState(addBooking, initialDate = null) { setAvailableTimeSlots([]); setSelectedStartIndex(null); setSelectedEndIndex(null); + setAssignedRoom(null); }, []); const resetSelections = useCallback(() => { @@ -75,7 +77,7 @@ export function useBookingState(addBooking, initialDate = null) { console.log('TimeCard clicked:', { startHour, hoursAvailable, roomId }); setSelectedStartIndex(startHour); setSelectedEndIndex(startHour + hoursAvailable); - setSelectedRoom(roomId); + setAssignedRoom(roomId); }, []); const handleDateChange = useCallback((date) => { @@ -107,10 +109,13 @@ export function useBookingState(addBooking, initialDate = null) { }, [resetTimeSelections]); const handleSave = useCallback(() => { + // Use assignedRoom for the booking, not selectedRoom + const roomToBook = selectedRoom !== "allRooms" ? selectedRoom : assignedRoom; + console.log('Saving booking with:', { selectedStartIndex, selectedEndIndex, - selectedRoom, + room: roomToBook, title }); @@ -124,8 +129,8 @@ export function useBookingState(addBooking, initialDate = null) { date: selectedDate, startTime: selectedStartIndex, endTime: selectedEndIndex, - room: selectedRoom, - roomCategory: getRoomCategory(selectedRoom), + room: roomToBook, + roomCategory: getRoomCategory(roomToBook), title: title !== "" ? title : DEFAULT_BOOKING_TITLE, participants: allParticipants }); @@ -133,7 +138,7 @@ export function useBookingState(addBooking, initialDate = null) { resetSelections(); navigate('/'); window.scrollTo(0, 0); - }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, title, participants, resetSelections, navigate]); + }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, assignedRoom, title, participants, resetSelections, navigate]); const handleTimeCardExit = useCallback(() => { if (!selectedEndIndex) { @@ -167,6 +172,7 @@ export function useBookingState(addBooking, initialDate = null) { timeSlotsByRoom, currentRoom, selectedRoom, + assignedRoom, selectedStartIndex, selectedEndIndex, selectedBookingLength, @@ -191,10 +197,12 @@ export function useBookingState(addBooking, initialDate = null) { handleTimeCardExit, handleParticipantChange, handleRemoveParticipant, + resetTimeSelections, }), [ timeSlotsByRoom, currentRoom, selectedRoom, + assignedRoom, selectedStartIndex, selectedEndIndex, selectedBookingLength, @@ -213,5 +221,6 @@ export function useBookingState(addBooking, initialDate = null) { handleTimeCardExit, handleParticipantChange, handleRemoveParticipant, + resetTimeSelections, ]); } \ No newline at end of file -- 2.39.5 From afa29dea950777a1abd92e25be2c80a8e5b379b3 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:08:45 +0200 Subject: [PATCH 35/41] filter button in new booking --- .../components/BookingLengthField.module.css | 1 - .../components/RoomSelectionField.module.css | 1 - my-app/src/pages/NewBooking.jsx | 74 ++++++++++- my-app/src/pages/NewBooking.module.css | 125 ++++++++++++++++++ 4 files changed, 195 insertions(+), 6 deletions(-) diff --git a/my-app/src/components/BookingLengthField.module.css b/my-app/src/components/BookingLengthField.module.css index f804eca..c5ad2cc 100644 --- a/my-app/src/components/BookingLengthField.module.css +++ b/my-app/src/components/BookingLengthField.module.css @@ -6,5 +6,4 @@ font-weight: 520; line-height: normal; margin-bottom: 0.2rem; - margin-top: 1.5rem; } \ No newline at end of file diff --git a/my-app/src/components/RoomSelectionField.module.css b/my-app/src/components/RoomSelectionField.module.css index f804eca..c5ad2cc 100644 --- a/my-app/src/components/RoomSelectionField.module.css +++ b/my-app/src/components/RoomSelectionField.module.css @@ -6,5 +6,4 @@ font-weight: 520; line-height: normal; margin-bottom: 0.2rem; - margin-top: 1.5rem; } \ No newline at end of file diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 0affa56..92d5249 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import styles from './NewBooking.module.css'; import { TimeCardContainer } from '../components/TimeCardContainer'; import { BookingDatePicker } from '../components/BookingDatePicker'; @@ -13,6 +13,43 @@ import { useSettingsContext } from '../context/SettingsContext'; export function NewBooking({ addBooking }) { const { getEffectiveToday } = useSettingsContext(); const booking = useBookingState(addBooking, getEffectiveToday()); + const [showFilters, setShowFilters] = useState(false); + + // Check if any filters are active + const hasActiveFilters = booking.selectedRoom !== "allRooms" || booking.selectedBookingLength > 0; + + // Generate filter display text + const getFilterText = () => { + const filters = []; + + if (booking.selectedRoom !== "allRooms") { + filters.push(booking.selectedRoom); + } + + if (booking.selectedBookingLength > 0) { + const bookingLengths = { + 1: "30 min", + 2: "1 h", + 3: "1.5 h", + 4: "2 h", + 5: "2.5 h", + 6: "3 h", + 7: "3.5 h", + 8: "4 h" + }; + filters.push(bookingLengths[booking.selectedBookingLength]); + } + + if (filters.length === 0) return "Filter"; + return `Filter (${filters.join(", ")})`; + }; + + // Reset all filters + const handleResetFilters = () => { + booking.handleRoomChange({ target: { value: "allRooms" } }); + booking.handleLengthChange(0); + setShowFilters(false); + }; return ( @@ -25,9 +62,38 @@ export function NewBooking({ addBooking }) {
-
- - + + {/* Filter Button */} +
+ + + {/* Collapsible Filter Content */} + {showFilters && ( +
+
+ + +
+ {hasActiveFilters && ( +
+ +
+ )} +
+ )}

diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css index 04885ca..9a3b2c4 100644 --- a/my-app/src/pages/NewBooking.module.css +++ b/my-app/src/pages/NewBooking.module.css @@ -142,4 +142,129 @@ background-color: rgb(216, 216, 216); width: 100%; margin: 1rem 0; +} + +/* Filter Section Styles */ +.filtersSection { + width: 100%; + margin-top: 1rem; + margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.filterButton { + width: fit-content; + background: white; + border: 1px solid #D1D5DB; + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.filterButton:hover { + background: #F9FAFB; + border-color: #9CA3AF; +} + +.filterButton:active { + background: #F3F4F6; +} + +.activeFilter { + background: #EBF8FF !important; + border-color: #3B82F6 !important; + color: #1E40AF !important; +} + +.activeFilter:hover { + background: #DBEAFE !important; + border-color: #2563EB !important; +} + +.filterIcon { + font-size: 1rem; +} + +.chevron { + font-size: 0.75rem; + color: #6B7280; + transition: transform 0.2s ease; +} + +.chevronUp { + transform: rotate(0deg); +} + +.chevronDown { + transform: rotate(0deg); +} + +.filtersContent { + width: fit-content; + max-width: 600px; + padding: 1rem; + background: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 0.5rem; + animation: slideDown 0.2s ease-out; +} + +.filtersRow { + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; +} + +.resetSection { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #E5E7EB; + display: flex; + justify-content: center; +} + +.resetButton { + background: white; + border: 1px solid #DC2626; + color: #DC2626; + border-radius: 0.375rem; + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.resetButton:hover { + background: #FEF2F2; + border-color: #B91C1C; + color: #B91C1C; +} + +.resetButton:active { + background: #FEE2E2; + transform: translateY(1px); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file -- 2.39.5 From 2633238209413f520e67194067d05f98b20b9510 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:45:09 +0200 Subject: [PATCH 36/41] new booking expansion instead of modal --- my-app/src/components/InlineBookingForm.jsx | 146 +++++++++++ .../components/InlineBookingForm.module.css | 243 ++++++++++++++++++ my-app/src/components/TimeCard.jsx | 58 ++--- my-app/src/components/TimeCard.module.css | 15 ++ my-app/src/components/TimeCardContainer.jsx | 72 ++++-- .../components/TimeCardContainer.module.css | 2 +- 6 files changed, 479 insertions(+), 57 deletions(-) create mode 100644 my-app/src/components/InlineBookingForm.jsx create mode 100644 my-app/src/components/InlineBookingForm.module.css diff --git a/my-app/src/components/InlineBookingForm.jsx b/my-app/src/components/InlineBookingForm.jsx new file mode 100644 index 0000000..01ba8c5 --- /dev/null +++ b/my-app/src/components/InlineBookingForm.jsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button } from 'react-aria-components'; +import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; +import Dropdown from './Dropdown'; +import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './InlineBookingForm.module.css'; + +export function InlineBookingForm({ + startTimeIndex, + hoursAvailable, + onClose, + arrowPointsLeft = true +}) { + const booking = useBookingContext(); + const { getCurrentUser } = useSettingsContext(); + + // Initialize with pre-selected booking length if available, or auto-select if only 30 min available + const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : + (hoursAvailable === 1 ? 1 : null); // Auto-select 30 min if that's all that's available + const [selectedLength, setSelectedLength] = useState(null); + const [calculatedEndTime, setCalculatedEndTime] = useState(startTimeIndex); + const hasInitialized = useRef(false); + + // Effect to handle initial setup only once when form opens + useEffect(() => { + if (initialLength && !hasInitialized.current) { + setSelectedLength(initialLength); + const newEndTime = startTimeIndex + initialLength; + setCalculatedEndTime(newEndTime); + booking.setSelectedEndIndex(newEndTime); + hasInitialized.current = true; + } + }, [initialLength, startTimeIndex, booking]); + + 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 handleChange(event) { + const lengthValue = event.target.value === "" ? null : parseInt(event.target.value); + setSelectedLength(lengthValue); + + if (lengthValue !== null) { + const newEndTime = startTimeIndex + lengthValue; + setCalculatedEndTime(newEndTime); + booking.setSelectedEndIndex(newEndTime); + } else { + // Reset to default state when placeholder is selected + setCalculatedEndTime(startTimeIndex); + booking.setSelectedEndIndex(null); + } + } + + // Check if user has selected a booking length (including pre-selected) + const hasSelectedLength = selectedLength !== null; + + // Display time range - show calculated end time if length is selected + const displayEndTime = hasSelectedLength ? calculatedEndTime : startTimeIndex; + + return ( +
+
+

{booking.title == "" ? "Jacobs bokning" : booking.title}

+

{convertDateObjectToString(booking.selectedDate)}

+
+ +
+
+
+ + {getTimeFromIndex(startTimeIndex)} +
+
+
+ + + {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} + +
+
+
+ +
+ + +
+ +
+ +

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

+
+ +
+ +

+ {(() => { + const currentUser = getCurrentUser(); + const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)]; + return allParticipants.map(p => p.name).join(", "); + })()} +

+
+ +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/components/InlineBookingForm.module.css b/my-app/src/components/InlineBookingForm.module.css new file mode 100644 index 0000000..8aa2c27 --- /dev/null +++ b/my-app/src/components/InlineBookingForm.module.css @@ -0,0 +1,243 @@ +.inlineForm { + background: white; + border: 1px solid #D1D5DB; + border-radius: 0.5rem; + padding: 1.5rem; + margin: 1rem 0; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + animation: slideDown 0.2s ease-out; + width: 100%; + flex-basis: 100%; + position: relative; +} + +/* Arrow pointing to left card */ +.arrowLeft::before { + content: ''; + position: absolute; + top: -8px; + left: 75px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #D1D5DB; +} + +.arrowLeft::after { + content: ''; + position: absolute; + top: -7px; + left: 76px; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid white; +} + +/* Arrow pointing to right card */ +.arrowRight::before { + content: ''; + position: absolute; + top: -8px; + right: 75px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #D1D5DB; +} + +.arrowRight::after { + content: ''; + position: absolute; + top: -7px; + right: 76px; + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid white; +} + +.formHeader { + text-align: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #E5E7EB; +} + +.formHeader h3 { + margin: 0 0 0.5rem 0; + font-size: 1.25rem; + font-weight: 700; + color: #111827; +} + +.dateText { + margin: 0; + color: #6B7280; + font-size: 0.875rem; +} + +.timeDisplay { + margin-bottom: 1.5rem; +} + +.timeRange { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 1rem; + background: #F9FAFB; + border-radius: 0.375rem; +} + +.timeItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.timeItem label { + font-size: 0.75rem; + color: #6B7280; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.timeValue { + font-size: 1.25rem; + font-weight: 600; + color: #111827; +} + +.timeValue.placeholder { + color: #9CA3AF; + font-style: italic; +} + +.timeSeparator { + font-size: 1.5rem; + color: #6B7280; + font-weight: 300; +} + +.formField { + margin-bottom: 1rem; +} + +.formField label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.5rem; +} + +.sectionWithTitle { + padding-top: 1rem; + display: flex; + flex-direction: column; + width: fit-content; +} + +.sectionWithTitle label { + font-size: 0.8rem; + color: #717171; +} + +.sectionWithTitle p { + margin: 0; +} + +.formActions { + display: flex; + gap: 1rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #E5E7EB; +} + +.cancelButton { + flex: 1; + background-color: white; + height: 2.75rem; + color: #374151; + font-weight: 600; + border: 2px solid #d1d5db; + border-radius: 0.375rem; + transition: all 0.2s ease; + cursor: pointer; + font-size: 0.875rem; +} + +.cancelButton:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +.cancelButton:active { + background-color: #e5e7eb; + transform: translateY(1px); +} + +.saveButton { + flex: 2; + background-color: #059669; + color: white; + height: 2.75rem; + font-weight: 600; + font-size: 0.875rem; + border: 2px solid #047857; + border-radius: 0.375rem; + 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); +} + +.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; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/my-app/src/components/TimeCard.jsx b/my-app/src/components/TimeCard.jsx index 145c86d..0111367 100644 --- a/my-app/src/components/TimeCard.jsx +++ b/my-app/src/components/TimeCard.jsx @@ -1,8 +1,7 @@ -import { Button, DialogTrigger } from 'react-aria-components'; -import React, { useState } from 'react'; +import { Button } from 'react-aria-components'; +import React from 'react'; import styles from './TimeCard.module.css'; import { useBookingContext } from '../context/BookingContext'; -import { BookingModal } from './BookingModal'; export default function TimeCard({ startTimeIndex, @@ -20,8 +19,6 @@ export default function TimeCard({ // Use the pre-selected booking length if available, otherwise use available hours const displayHours = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : hoursAvailable; const halfHours = displayHours; - - const [endTimeIndex, setEndTimeIndex] = useState(startTimeIndex + hoursAvailable); if (halfHours === 1) { hoursText = "30\u202Fmin"; @@ -41,7 +38,7 @@ export default function TimeCard({ return `${hour}:${minute}`; } - let classNames = selected ? `${styles.container}` : styles.container; + let classNames = selected ? `${styles.container} ${styles.selected}` : styles.container; const className = state === "unavailableSlot" ? styles.unavailableSlot : styles.availableSlot; @@ -52,37 +49,24 @@ export default function TimeCard({ if (state === "availableSlot") { return ( - - - { - // Reset time selections when modal is closed without booking - booking.resetTimeSelections(); - }} - /> - + ); } diff --git a/my-app/src/components/TimeCard.module.css b/my-app/src/components/TimeCard.module.css index 2bc1f44..98f4a06 100644 --- a/my-app/src/components/TimeCard.module.css +++ b/my-app/src/components/TimeCard.module.css @@ -35,6 +35,21 @@ outline-offset: -1px; } +.selected { + background-color: #2563EB !important; + color: white !important; + border-color: #1d4ed8 !important; + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); +} + +.selected .upToText { + color: rgba(255, 255, 255, 0.8) !important; +} + +.selected .hoursText { + color: white !important; +} + .container p { margin: 0; } diff --git a/my-app/src/components/TimeCardContainer.jsx b/my-app/src/components/TimeCardContainer.jsx index 4c30e1c..f77a86a 100644 --- a/my-app/src/components/TimeCardContainer.jsx +++ b/my-app/src/components/TimeCardContainer.jsx @@ -1,5 +1,6 @@ import React from 'react'; import TimeCard from './TimeCard'; +import { InlineBookingForm } from './InlineBookingForm'; import styles from './TimeCardContainer.module.css'; import { useBookingContext } from '../context/BookingContext'; @@ -42,25 +43,24 @@ export function TimeCardContainer() { } return ( -
- {slotIndiciesToColumns(slotIndices).map((column, index) => { - - return ( -
- { - - column.map(index => { +
+
+ {slotIndiciesToColumns(slotIndices).map((column, index) => { + + return ( +
+ {column.map(slotIndex => { let maxConsecutive = 0; let roomId = ""; if (booking.currentRoom) { - const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, index); + const consecutive = countConsecutiveFromSlot(booking.currentRoom.times, slotIndex); if (consecutive > maxConsecutive) { maxConsecutive = consecutive; } } else { booking.timeSlotsByRoom.forEach(room => { - const consecutive = countConsecutiveFromSlot(room.times, index); + const consecutive = countConsecutiveFromSlot(room.times, slotIndex); if (consecutive > maxConsecutive) { maxConsecutive = consecutive; roomId = room.roomId; @@ -79,8 +79,8 @@ export function TimeCardContainer() { if (booking.selectedBookingLength !== 0) { // Check if this slot can accommodate the selected booking length const actualConsecutive = booking.currentRoom ? - countConsecutiveFromSlot(booking.currentRoom.times, index) : - Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, index))); + countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) : + Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex))); if (actualConsecutive >= booking.selectedBookingLength) { timeCardState = "availableSlot"; @@ -91,23 +91,57 @@ export function TimeCardContainer() { timeCardState = "availableSlot"; } } - - return ( + + const elements = []; + + elements.push( booking.handleTimeCardClick(index, maxConsecutive, roomId)} - selected={index === booking.selectedStartIndex} + handleClick={() => { + if (booking.selectedStartIndex === slotIndex) { + // If clicking on already selected card, close the form + booking.resetTimeSelections(); + } else { + // Otherwise, select this card + booking.handleTimeCardClick(slotIndex, maxConsecutive, roomId); + } + }} + selected={slotIndex === booking.selectedStartIndex} state={timeCardState} handleTimeCardHover={() => {}} handleTimeCardExit={booking.handleTimeCardExit} /> ); - })} + + // Add inline booking form after the pair that contains the selected time card + // Cards are laid out in pairs: (0,1), (2,3), (4,5), etc. + if (booking.selectedStartIndex !== null) { + const selectedPairStart = Math.floor(booking.selectedStartIndex / 2) * 2; + const selectedPairEnd = selectedPairStart + 1; + + // Show form after the second card of the pair that contains the selected card + if (slotIndex === selectedPairEnd && (booking.selectedStartIndex === selectedPairStart || booking.selectedStartIndex === selectedPairEnd)) { + const isLeftCard = booking.selectedStartIndex === selectedPairStart; + elements.push( + booking.resetTimeSelections()} + arrowPointsLeft={isLeftCard} + /> + ); + } + } + + return elements; + }).flat()}
) })} +
); } \ No newline at end of file diff --git a/my-app/src/components/TimeCardContainer.module.css b/my-app/src/components/TimeCardContainer.module.css index 012d802..aa8fd9f 100644 --- a/my-app/src/components/TimeCardContainer.module.css +++ b/my-app/src/components/TimeCardContainer.module.css @@ -13,7 +13,7 @@ width: 350px; gap: 0.5rem; height: fit-content; - align-items: center; + align-items: flex-start; justify-content: center; } -- 2.39.5 From b1b3fd434c701efe3e5a84bb4bd1ec47e0ee88b5 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:52:36 +0200 Subject: [PATCH 37/41] inline booking --- my-app/src/components/InlineBookingForm.jsx | 86 ++++++++++--------- .../components/InlineBookingForm.module.css | 19 ++-- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/my-app/src/components/InlineBookingForm.jsx b/my-app/src/components/InlineBookingForm.jsx index 01ba8c5..7a3396a 100644 --- a/my-app/src/components/InlineBookingForm.jsx +++ b/my-app/src/components/InlineBookingForm.jsx @@ -2,6 +2,8 @@ import React, { useState, useEffect, useRef } from 'react'; import { Button } from 'react-aria-components'; import { convertDateObjectToString, getTimeFromIndex } from '../helpers'; import Dropdown from './Dropdown'; +import { BookingTitleField } from './BookingTitleField'; +import { ParticipantsSelector } from './ParticipantsSelector'; import { useBookingContext } from '../context/BookingContext'; import { useSettingsContext } from '../context/SettingsContext'; import styles from './InlineBookingForm.module.css'; @@ -78,57 +80,63 @@ export function InlineBookingForm({ return (
+ {/* Date Context */}
-

{booking.title == "" ? "Jacobs bokning" : booking.title}

{convertDateObjectToString(booking.selectedDate)}

-
-
-
- - {getTimeFromIndex(startTimeIndex)} -
-
-
- - - {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} - + {/* Title - What */} +
+ +
+ + {/* Time Selection - When */} +
+
+
+
+ + {getTimeFromIndex(startTimeIndex)} +
+
+
+ + + {hasSelectedLength ? getTimeFromIndex(displayEndTime) : "Välj längd"} + +
+ +
+ + +
-
- - + {/* Participants - Who */} +
+
-
- -

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

-
- -
- -

- {(() => { - const currentUser = getCurrentUser(); - const allParticipants = [currentUser, ...booking.participants.filter(p => p.id !== currentUser.id)]; - return allParticipants.map(p => p.name).join(", "); - })()} -

+ {/* Room - Where */} +
+
+ +

{booking.selectedRoom !== "allRooms" ? booking.selectedRoom : (booking.assignedRoom || 'Inget rum tilldelat')}

+
+ {/* Actions */}
+ + {/* Show modal when a time slot is selected and not using inline form */} + {!useInlineForm && booking.selectedStartIndex !== null && ( + booking.resetTimeSelections()} + isOpen={true} + /> + )}
); } \ No newline at end of file diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index d124d19..515011f 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -31,6 +31,7 @@ export const SettingsProvider = ({ children }) => { showDevelopmentBanner: false, showBookingConfirmationBanner: false, showBookingDeleteBanner: false, + bookingFormType: 'inline', // 'modal' or 'inline' // Then override with saved values ...parsed, // Convert date strings back to DateValue objects @@ -64,6 +65,8 @@ export const SettingsProvider = ({ children }) => { showBookingConfirmationBanner: false, // Booking delete banner toggle showBookingDeleteBanner: false, + // Booking form type + bookingFormType: 'inline', // 'modal' or 'inline' }; }); @@ -104,6 +107,10 @@ export const SettingsProvider = ({ children }) => { earliestTimeSlot: 0, latestTimeSlot: 23, currentUserName: USER.name, + showDevelopmentBanner: false, + showBookingConfirmationBanner: false, + showBookingDeleteBanner: false, + bookingFormType: 'inline', }); localStorage.removeItem('calendarSettings'); }; diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 8f6cfb7..9dddd95 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -128,6 +128,27 @@ export function BookingSettings() {
+ +
+ + +
+ Current: {settings.bookingFormType === 'inline' ? 'Inline Form' : 'Modal Popup'} +
+
diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx index 92d5249..add31ec 100644 --- a/my-app/src/pages/NewBooking.jsx +++ b/my-app/src/pages/NewBooking.jsx @@ -11,10 +11,13 @@ import { BookingProvider } from '../context/BookingContext'; import { useSettingsContext } from '../context/SettingsContext'; export function NewBooking({ addBooking }) { - const { getEffectiveToday } = useSettingsContext(); + const { getEffectiveToday, settings } = useSettingsContext(); const booking = useBookingState(addBooking, getEffectiveToday()); const [showFilters, setShowFilters] = useState(false); + // Check if we should use inline form (hide title and participants from main form) + const useInlineForm = settings.bookingFormType === 'inline'; + // Check if any filters are active const hasActiveFilters = booking.selectedRoom !== "allRooms" || booking.selectedBookingLength > 0; @@ -57,8 +60,13 @@ export function NewBooking({ addBooking }) {

Boka litet grupprum

- - + {/* Only show title and participants fields in modal mode */} + {!useInlineForm && ( + <> + + + + )}
-- 2.39.5 From 3ec26f789ae6816bffcebeedd1fee9839fe7915f Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:42:50 +0200 Subject: [PATCH 39/41] Test session function --- my-app/src/AppRoutes.jsx | 4 + my-app/src/components/BookingModal.jsx | 4 +- my-app/src/components/BookingTitleField.jsx | 5 +- .../src/components/ParticipantsSelector.jsx | 4 +- my-app/src/context/SettingsContext.jsx | 9 +- my-app/src/hooks/useBookingState.js | 8 +- my-app/src/pages/BookingSettings.jsx | 6 + my-app/src/pages/BookingSettings.module.css | 17 +++ my-app/src/pages/TestSession.jsx | 84 ++++++++++++++ my-app/src/pages/TestSession.module.css | 103 ++++++++++++++++++ 10 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 my-app/src/pages/TestSession.jsx create mode 100644 my-app/src/pages/TestSession.module.css diff --git a/my-app/src/AppRoutes.jsx b/my-app/src/AppRoutes.jsx index 86fcff2..2c0b2da 100644 --- a/my-app/src/AppRoutes.jsx +++ b/my-app/src/AppRoutes.jsx @@ -6,6 +6,7 @@ import Layout from './Layout'; import { RoomBooking } from './pages/RoomBooking'; import { NewBooking } from './pages/NewBooking'; import { BookingSettings } from './pages/BookingSettings'; +import { TestSession } from './pages/TestSession'; import FullScreenLoader from './components/FullScreenLoader'; const AppRoutes = () => { @@ -105,6 +106,9 @@ const AppRoutes = () => { + {/* Fullscreen route outside of Layout */} + } /> + }> setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} /> } /> diff --git a/my-app/src/components/BookingModal.jsx b/my-app/src/components/BookingModal.jsx index bd05f3b..0a4945f 100644 --- a/my-app/src/components/BookingModal.jsx +++ b/my-app/src/components/BookingModal.jsx @@ -16,7 +16,7 @@ export function BookingModal({ isOpen = true }) { const booking = useBookingContext(); - const { getCurrentUser } = useSettingsContext(); + const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext(); // Initialize with pre-selected booking length if available, or auto-select if only 30 min available const initialLength = booking.selectedBookingLength > 0 ? booking.selectedBookingLength : @@ -98,7 +98,7 @@ export function BookingModal({ > - {booking.title == "" ? "Jacobs bokning" : booking.title} + {booking.title == "" ? getDefaultBookingTitle() : booking.title}

{convertDateObjectToString(booking.selectedDate)}

diff --git a/my-app/src/components/BookingTitleField.jsx b/my-app/src/components/BookingTitleField.jsx index cd02fcd..71eca53 100644 --- a/my-app/src/components/BookingTitleField.jsx +++ b/my-app/src/components/BookingTitleField.jsx @@ -1,10 +1,11 @@ import React from 'react'; -import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; import styles from './BookingTitleField.module.css'; export function BookingTitleField({ compact = false }) { const booking = useBookingContext(); + const { getDefaultBookingTitle } = useSettingsContext(); return ( <> @@ -13,7 +14,7 @@ export function BookingTitleField({ compact = false }) { type="text" value={booking.title} onChange={(event) => booking.setTitle(event.target.value)} - placeholder={DEFAULT_BOOKING_TITLE} + placeholder={getDefaultBookingTitle()} className={compact ? styles.compactTextInput : styles.textInput} /> diff --git a/my-app/src/components/ParticipantsSelector.jsx b/my-app/src/components/ParticipantsSelector.jsx index ac061fe..d3f1d9d 100644 --- a/my-app/src/components/ParticipantsSelector.jsx +++ b/my-app/src/components/ParticipantsSelector.jsx @@ -1,10 +1,12 @@ import React, { useState, useRef, useEffect } from 'react'; import { PEOPLE, USER } from '../constants/bookingConstants'; import { useBookingContext } from '../context/BookingContext'; +import { useSettingsContext } from '../context/SettingsContext'; import styles from './ParticipantsSelector.module.css'; export function ParticipantsSelector({ compact = false }) { const booking = useBookingContext(); + const { getCurrentUser } = useSettingsContext(); const [searchTerm, setSearchTerm] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); @@ -266,7 +268,7 @@ export function ParticipantsSelector({ compact = false }) {
{/* Default User (Non-deletable) */}
- {USER.name} + {getCurrentUser().name}
{/* Additional Participants (Deletable) */} diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx index 515011f..b287737 100644 --- a/my-app/src/context/SettingsContext.jsx +++ b/my-app/src/context/SettingsContext.jsx @@ -106,7 +106,7 @@ export const SettingsProvider = ({ children }) => { numberOfRooms: 5, earliestTimeSlot: 0, latestTimeSlot: 23, - currentUserName: USER.name, + currentUserName: USER.name, // This will reset to "Jacob Reinikainen" showDevelopmentBanner: false, showBookingConfirmationBanner: false, showBookingDeleteBanner: false, @@ -125,6 +125,12 @@ export const SettingsProvider = ({ children }) => { }; }; + // Get dynamic default booking title + const getDefaultBookingTitle = () => { + const firstName = settings.currentUserName.split(' ')[0]; + return `${firstName}s bokning`; + }; + return ( { resetSettings, getEffectiveToday, getCurrentUser, + getDefaultBookingTitle, }}> {children} diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js index 44245b7..e77b0c4 100644 --- a/my-app/src/hooks/useBookingState.js +++ b/my-app/src/hooks/useBookingState.js @@ -6,7 +6,7 @@ import { generateId, findObjectById } from '../utils/bookingUtils'; -import { DEFAULT_BOOKING_TITLE, PEOPLE, USER } from '../constants/bookingConstants'; +import { PEOPLE, USER } from '../constants/bookingConstants'; import { useDisabledOptions } from './useDisabledOptions'; import { useSettingsContext } from '../context/SettingsContext'; @@ -25,7 +25,7 @@ function getRoomCategory(roomName) { } export function useBookingState(addBooking, initialDate = null) { - const { settings } = useSettingsContext(); + const { settings, getDefaultBookingTitle } = useSettingsContext(); // State hooks - simplified back to useState for stability const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(() => @@ -131,14 +131,14 @@ export function useBookingState(addBooking, initialDate = null) { endTime: selectedEndIndex, room: roomToBook, roomCategory: getRoomCategory(roomToBook), - title: title !== "" ? title : DEFAULT_BOOKING_TITLE, + title: title !== "" ? title : getDefaultBookingTitle(), participants: allParticipants }); resetSelections(); navigate('/'); window.scrollTo(0, 0); - }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, assignedRoom, title, participants, resetSelections, navigate]); + }, [addBooking, selectedDate, selectedStartIndex, selectedEndIndex, selectedRoom, assignedRoom, title, participants, resetSelections, navigate, getDefaultBookingTitle]); const handleTimeCardExit = useCallback(() => { if (!selectedEndIndex) { diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx index 9dddd95..2b2e67b 100644 --- a/my-app/src/pages/BookingSettings.jsx +++ b/my-app/src/pages/BookingSettings.jsx @@ -305,6 +305,12 @@ export function BookingSettings() { +
Settings are automatically saved and will persist between sessions
diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css index 6fe559e..a419d3c 100644 --- a/my-app/src/pages/BookingSettings.module.css +++ b/my-app/src/pages/BookingSettings.module.css @@ -258,6 +258,23 @@ background: #b91c1c; } +.testSessionButton { + padding: 0.75rem 2rem; + background: #2563eb; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + margin-left: 1rem; +} + +.testSessionButton:hover { + background: #1d4ed8; +} + .info { font-size: 0.875rem; color: #6b7280; diff --git a/my-app/src/pages/TestSession.jsx b/my-app/src/pages/TestSession.jsx new file mode 100644 index 0000000..02f9dfd --- /dev/null +++ b/my-app/src/pages/TestSession.jsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSettingsContext } from '../context/SettingsContext'; +import styles from './TestSession.module.css'; + +const NORMAL_NAMES = [ + "Nigel Twittlebottom", + "Percival Crumplebottom", + "Reginald Puddingworth", + "Algernon Snodgrass", + "Bartholomew Wigglesworth" +]; + +export function TestSession() { + const [userName, setUserName] = useState(''); + const [isUsingNormalName, setIsUsingNormalName] = useState(false); + const { updateSettings } = useSettingsContext(); + const navigate = useNavigate(); + + const handleNormalNameSelect = (name) => { + setUserName(name); + setIsUsingNormalName(true); + }; + + const handleCustomNameChange = (e) => { + setUserName(e.target.value); + setIsUsingNormalName(false); + }; + + const handleStart = () => { + if (userName.trim()) { + // Update the user name in settings + updateSettings({ currentUserName: userName.trim() }); + // Navigate to the main app + navigate('/'); + } + }; + + const canStart = userName.trim().length > 0; + + return ( +
+
+
+

Test Session

+

Enter your name to begin:

+
+ +
+ + +

Or choose one:

+ +
+ {NORMAL_NAMES.map((name, index) => ( + + ))} +
+
+ + {canStart && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/pages/TestSession.module.css b/my-app/src/pages/TestSession.module.css new file mode 100644 index 0000000..3762201 --- /dev/null +++ b/my-app/src/pages/TestSession.module.css @@ -0,0 +1,103 @@ +.container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.content { + max-width: 500px; + width: 100%; + text-align: center; +} + +.header h1 { + font-size: 2rem; + color: #1f2937; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.header p { + font-size: 1rem; + color: #6b7280; + margin-bottom: 2rem; +} + +.nameSection { + margin: 2rem 0; +} + +.nameInput { + width: 100%; + padding: 0.75rem 1rem; + font-size: 1rem; + border: 1px solid #d1d5db; + border-radius: 6px; + outline: none; + margin-bottom: 1.5rem; +} + +.nameInput:focus { + border-color: #2563eb; +} + +.normalNames { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; +} + +.normalNameButton { + padding: 0.5rem 1rem; + background: white; + border: 1px solid #d1d5db; + border-radius: 4px; + font-size: 0.9rem; + color: #374151; + cursor: pointer; + margin: 0.25rem; +} + +.normalNameButton:hover { + background: #f9fafb; +} + +.normalNameButton.selected { + background: #2563eb; + border-color: #2563eb; + color: white; +} + +.startButton { + padding: 0.75rem 2rem; + background: #2563eb; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; + margin-top: 1.5rem; +} + +.startButton:hover { + background: #1d4ed8; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .normalNames { + flex-direction: column; + align-items: center; + } + + .normalNameButton { + width: 100%; + max-width: 200px; + } +} \ No newline at end of file -- 2.39.5 From 2a8a7ea0cbc4cea20c40b0b4de1f9f4be852a0d3 Mon Sep 17 00:00:00 2001 From: Jacob Reinikainen <80760206+jazzjacob@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:54:06 +0200 Subject: [PATCH 40/41] simplify test session --- my-app/src/pages/BookingSettings.module.css | 6 +++++- my-app/src/pages/TestSession.jsx | 21 +++------------------ my-app/src/pages/TestSession.module.css | 8 +++++++- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css index a419d3c..9e70107 100644 --- a/my-app/src/pages/BookingSettings.module.css +++ b/my-app/src/pages/BookingSettings.module.css @@ -118,6 +118,7 @@ color: #374151; cursor: pointer; transition: all 0.2s; + width: fit-content; } .clearButton:hover { @@ -235,11 +236,13 @@ display: flex; flex-direction: column; align-items: center; + justify-content: center;; gap: 1rem; padding: 2rem; background: #f9fafb; border-radius: 12px; border: 1px solid #e5e7eb; + width: 100%; } .resetButton { @@ -252,6 +255,7 @@ font-weight: 600; cursor: pointer; transition: background-color 0.2s; + width: fit-content; } .resetButton:hover { @@ -268,7 +272,7 @@ font-weight: 600; cursor: pointer; transition: background-color 0.2s; - margin-left: 1rem; + width: fit-content; } .testSessionButton:hover { diff --git a/my-app/src/pages/TestSession.jsx b/my-app/src/pages/TestSession.jsx index 02f9dfd..abac7f7 100644 --- a/my-app/src/pages/TestSession.jsx +++ b/my-app/src/pages/TestSession.jsx @@ -42,8 +42,8 @@ export function TestSession() {
-

Test Session

-

Enter your name to begin:

+

Testsession

+

Välommen! Ange ditt namn för att börja:

@@ -51,25 +51,10 @@ export function TestSession() { type="text" value={userName} onChange={handleCustomNameChange} - placeholder="Your name" + placeholder="För- och efternamn" className={styles.nameInput} /> - -

Or choose one:

- -
- {NORMAL_NAMES.map((name, index) => ( - - ))} -
- {canStart && (