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 (
+
+
+
+ );
+}
+
+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'}}>
-
-
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({
>