- {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;
@@ -73,26 +81,88 @@ export function TimeCardContainer() {
/* Set time card state here: */
let timeCardState = "unavailableSlot";
- if (maxConsecutive > 0) {
- timeCardState = "availableSlot";
- }
+
+ // If a booking length is pre-selected, only show slots that can accommodate the exact length
+ if (booking.selectedBookingLength !== 0) {
+ // Check if this slot can accommodate the selected booking length
+ const actualConsecutive = booking.currentRoom ?
+ countConsecutiveFromSlot(booking.currentRoom.times, slotIndex) :
+ Math.max(...booking.timeSlotsByRoom.map(room => countConsecutiveFromSlot(room.times, slotIndex)));
- return (
+ if (actualConsecutive >= booking.selectedBookingLength) {
+ timeCardState = "availableSlot";
+ }
+ } else {
+ // No pre-selected length, show if any time is available
+ if (maxConsecutive > 0) {
+ timeCardState = "availableSlot";
+ }
+ }
+
+ 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
+ // Only show inline form if useInlineForm is true
+ // Cards are laid out in pairs: (0,1), (2,3), (4,5), etc.
+ if (useInlineForm && 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()}
)
})}
+
+
+ {/* 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/components/TimeCardContainer.module.css b/my-app/src/components/TimeCardContainer.module.css
index 5e44012..aa8fd9f 100644
--- a/my-app/src/components/TimeCardContainer.module.css
+++ b/my-app/src/components/TimeCardContainer.module.css
@@ -13,6 +13,8 @@
width: 350px;
gap: 0.5rem;
height: fit-content;
+ align-items: flex-start;
+ justify-content: center;
}
.timeCardList {
diff --git a/my-app/src/constants/bookingConstants.js b/my-app/src/constants/bookingConstants.js
index af4eba1..0627e0c 100644
--- a/my-app/src/constants/bookingConstants.js
+++ b/my-app/src/constants/bookingConstants.js
@@ -19,14 +19,273 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({
}));
export const PEOPLE = [
- { id: 1, name: 'Arjohn Emilsson' },
- { id: 2, name: 'Filip Norgren' },
- { id: 3, name: 'Hedvig Engelmark' },
- { id: 4, name: 'Elin Rudling' },
- { id: 5, name: 'Victor Magnusson' },
- { id: 6, name: 'Ellen Britschgi' }
+ { id: 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' },
+ { id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' },
+ { id: 6, name: 'Ellen Britschgi', username: 'elbr5623', email: 'ellen.britschgi@dsv.su.se' },
+ { id: 7, name: 'Anna Andersson', username: 'anan3457', email: 'anna.andersson@dsv.su.se' },
+ { id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' },
+ { id: 9, name: 'Sofia Karlsson', username: 'soka1245', email: 'sofia.karlsson@dsv.su.se' },
+ { id: 10, name: 'Magnus Nilsson', username: 'mani6789', email: 'magnus.nilsson@dsv.su.se' },
+ { id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' },
+ { id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' },
+ { id: 13, name: 'Linda Svensson', username: 'lisv2174', email: 'linda.svensson@dsv.su.se' },
+ { id: 14, name: 'Jonas Gustafsson', username: 'jogu8523', email: 'jonas.gustafsson@dsv.su.se' },
+ { id: 15, name: 'Maria Olsson', username: 'maol7456', email: 'maria.olsson@dsv.su.se' },
+ { id: 16, name: 'Andreas Berg', username: 'anbe9832', email: 'andreas.berg@dsv.su.se' },
+ { id: 17, name: 'Lina Dahlberg', username: 'lida2165', email: 'lina.dahlberg@dsv.su.se' },
+ { id: 18, name: 'Marcus Forsberg', username: 'mafo6754', email: 'marcus.forsberg@dsv.su.se' },
+ { id: 19, name: 'Julia Hedström', username: 'juhe4921', email: 'julia.hedström@dsv.su.se' },
+ { id: 20, name: 'Daniel Lindqvist', username: 'dali8374', email: 'daniel.lindqvist@dsv.su.se' },
+ { id: 21, name: 'Sara Blomqvist', username: 'sabl1598', email: 'sara.blomqvist@dsv.su.se' },
+ { id: 22, name: 'Henrik Lundberg', username: 'helu7263', email: 'henrik.lundberg@dsv.su.se' },
+ { id: 23, name: 'Frida Engström', username: 'fren3847', email: 'frida.engström@dsv.su.se' },
+ { id: 24, name: 'Tobias Sjöberg', username: 'tosj5691', email: 'tobias.sjöberg@dsv.su.se' },
+ { id: 25, name: 'Amanda Wallin', username: 'amwa9254', email: 'amanda.wallin@dsv.su.se' },
+ { id: 26, name: 'Mattias Holm', username: 'maho8173', email: 'mattias.holm@dsv.su.se' },
+ { id: 27, name: 'Emelie Sandberg', username: 'emsa4629', email: 'emelie.sandberg@dsv.su.se' },
+ { id: 28, name: 'Robin Åberg', username: 'roab7485', email: 'robin.åberg@dsv.su.se' },
+ { id: 29, name: 'Caroline Ekström', username: 'caek2916', email: 'caroline.ekström@dsv.su.se' },
+ { id: 30, name: 'Alexander Nyström', username: 'alny6387', email: 'alexander.nyström@dsv.su.se' },
+ { id: 31, name: 'Lisa Berggren', username: 'libe5142', email: 'lisa.berggren@dsv.su.se' },
+ { id: 32, name: 'David Holmberg', username: 'daho8764', email: 'david.holmberg@dsv.su.se' },
+ { id: 33, name: 'Maja Lindström', username: 'mali3571', email: 'maja.lindström@dsv.su.se' },
+ { id: 34, name: 'Johan Carlsson', username: 'joca6928', email: 'johan.carlsson@dsv.su.se' },
+ { id: 35, name: 'Rebecka Svensson', username: 'resv4816', email: 'rebecka.svensson@dsv.su.se' },
+ { id: 36, name: 'Niklas Hedberg', username: 'nihe7395', email: 'niklas.hedberg@dsv.su.se' },
+ { id: 37, name: 'Ida Persson', username: 'idpe2583', email: 'ida.persson@dsv.su.se' },
+ { id: 38, name: 'Martin Öberg', username: 'maöb5947', email: 'martin.öberg@dsv.su.se' },
+ { id: 39, name: 'Therese Löfgren', username: 'thlo8162', email: 'therese.löfgren@dsv.su.se' },
+ { id: 40, name: 'Stefan Martinsson', username: 'stma4739', email: 'stefan.martinsson@dsv.su.se' },
+ { id: 41, name: 'Johanna Stenberg', username: 'jost6254', email: 'johanna.stenberg@dsv.su.se' },
+ { id: 42, name: 'Peter Wikström', username: 'pewi9481', email: 'peter.wikström@dsv.su.se' },
+ { id: 43, name: 'Mikaela Fransson', username: 'mifr3825', email: 'mikaela.fransson@dsv.su.se' },
+ { id: 44, name: 'Christian Lindgren', username: 'chli7694', email: 'christian.lindgren@dsv.su.se' },
+ { id: 45, name: 'Evelina Norberg', username: 'evno2147', email: 'evelina.norberg@dsv.su.se' },
+ { id: 46, name: 'Andreas Strömberg', username: 'anst5863', email: 'andreas.strömberg@dsv.su.se' },
+ { id: 47, name: 'Klara Danielsson', username: 'klda8429', email: 'klara.danielsson@dsv.su.se' },
+ { id: 48, name: 'Simon Eklund', username: 'siek6175', email: 'simon.eklund@dsv.su.se' },
+ { id: 49, name: 'Elsa Lundgren', username: 'ellu4892', email: 'elsa.lundgren@dsv.su.se' },
+ { id: 50, name: 'Mikael Höglund', username: 'mihö7316', email: 'mikael.höglund@dsv.su.se' },
+ { id: 51, name: 'Jennie Söderberg', username: 'jesö3754', email: 'jennie.söderberg@dsv.su.se' },
+ { id: 52, name: 'Fredrik Åström', username: 'frås9128', email: 'fredrik.åström@dsv.su.se' },
+ { id: 53, name: 'Cornelia Sundberg', username: 'cosu5683', email: 'cornelia.sundberg@dsv.su.se' },
+ { id: 54, name: 'Emil Palmberg', username: 'empa8297', email: 'emil.palmberg@dsv.su.se' },
+ { id: 55, name: 'Josefin Ringström', username: 'jori4612', email: 'josefin.ringström@dsv.su.se' },
+ { id: 56, name: 'Christofer Åkesson', username: 'chåk7548', email: 'christofer.åkesson@dsv.su.se' },
+ { id: 57, name: 'Agnes Håkansson', username: 'aghå2971', email: 'agnes.håkansson@dsv.su.se' },
+ { id: 58, name: 'Sebastian Blomberg', username: 'sebl6394', email: 'sebastian.blomberg@dsv.su.se' },
+ { id: 59, name: 'Felicia Gunnarsson', username: 'fegu8756', email: 'felicia.gunnarsson@dsv.su.se' },
+ { id: 60, name: 'Jesper Lindahl', username: 'jeli3182', email: 'jesper.lindahl@dsv.su.se' },
+ { id: 61, name: 'Sandra Backström', username: 'saba7425', email: 'sandra.backström@dsv.su.se' },
+ { id: 62, name: 'William Viklund', username: 'wivi5697', email: 'william.viklund@dsv.su.se' },
+ { id: 63, name: 'Lova Arvidsson', username: 'loar9231', email: 'lova.arvidsson@dsv.su.se' },
+ { id: 64, name: 'Gabriel Öhman', username: 'gaöh4568', email: 'gabriel.öhman@dsv.su.se' },
+ { id: 65, name: 'Isabelle Eriksson', username: 'iser8143', email: 'isabelle.eriksson@dsv.su.se' },
+ { id: 66, name: 'Anton Ström', username: 'anst2976', email: 'anton.ström@dsv.su.se' },
+ { id: 67, name: 'Wilma Ljungberg', username: 'wilj6314', email: 'wilma.ljungberg@dsv.su.se' },
+ { id: 68, name: 'Lucas Sundström', username: 'lusu9587', email: 'lucas.sundström@dsv.su.se' },
+ { id: 69, name: 'Hanna Åslund', username: 'haås4729', email: 'hanna.åslund@dsv.su.se' },
+ { id: 70, name: 'Pontus Rydberg', username: 'pory8152', email: 'pontus.rydberg@dsv.su.se' },
+ { id: 71, name: 'Olivia Nyman', username: 'olny3675', email: 'olivia.nyman@dsv.su.se' },
+ { id: 72, name: 'Viktor Östberg', username: 'viös7493', email: 'viktor.östberg@dsv.su.se' },
+ { id: 73, name: 'Tilda Forslund', username: 'tifo5128', email: 'tilda.forslund@dsv.su.se' },
+ { id: 74, name: 'Carl Holmström', username: 'caho8916', email: 'carl.holmström@dsv.su.se' },
+ { id: 75, name: 'Matilda Bengtsson', username: 'mabe4382', email: 'matilda.bengtsson@dsv.su.se' },
+ { id: 76, name: 'Alvin Berglund', username: 'albe7654', email: 'alvin.berglund@dsv.su.se' },
+ { id: 77, name: 'Saga Nordström', username: 'sano2496', email: 'saga.nordström@dsv.su.se' },
+ { id: 78, name: 'Linus Hedström', username: 'lihe6847', email: 'linus.hedström@dsv.su.se' },
+ { id: 79, name: 'Elina Jakobsson', username: 'elja9173', email: 'elina.jakobsson@dsv.su.se' },
+ { id: 80, name: 'Casper Nordin', username: 'cano3521', email: 'casper.nordin@dsv.su.se' },
+ { id: 81, name: 'Nova Malmberg', username: 'noma8765', email: 'nova.malmberg@dsv.su.se' },
+ { id: 82, name: 'Isac Björk', username: 'isbj5194', email: 'isac.björk@dsv.su.se' },
+ { id: 83, name: 'Ebba Sandström', username: 'ebsa7428', email: 'ebba.sandström@dsv.su.se' },
+ { id: 84, name: 'Melvin Åberg', username: 'meåb2683', email: 'melvin.åberg@dsv.su.se' },
+ { id: 85, name: 'Astrid Nordahl', username: 'asno6159', email: 'astrid.nordahl@dsv.su.se' },
+ { id: 86, name: 'Noel Sjögren', username: 'nosj8437', email: 'noel.sjögren@dsv.su.se' },
+ { id: 87, name: 'Linnéa Borg', username: 'libo4826', email: 'linnéa.borg@dsv.su.se' },
+ { id: 88, name: 'Adrian Rosén', username: 'adro7195', email: 'adrian.rosén@dsv.su.se' },
+ { id: 89, name: 'Smilla Lindberg', username: 'smli3564', email: 'smilla.lindberg@dsv.su.se' },
+ { id: 90, name: 'Leon Hammar', username: 'leha9821', email: 'leon.hammar@dsv.su.se' },
+ { id: 91, name: 'Ellen Sjöström', username: 'elsj6247', email: 'ellen.sjöström@dsv.su.se' },
+ { id: 92, name: 'Tim Hedlund', username: 'tihe4573', email: 'tim.hedlund@dsv.su.se' },
+ { id: 93, name: 'Vera Blomgren', username: 'vebl8912', email: 'vera.blomgren@dsv.su.se' },
+ { id: 94, name: 'Theodor Larsson', username: 'thla2738', email: 'theodor.larsson@dsv.su.se' },
+ { id: 95, name: 'Stella Lundström', username: 'stlu6495', email: 'stella.lundström@dsv.su.se' },
+ { id: 96, name: 'Benjamin Engberg', username: 'been8164', email: 'benjamin.engberg@dsv.su.se' },
+ { id: 97, name: 'Alicia Rydberg', username: 'alry4827', email: 'alicia.rydberg@dsv.su.se' },
+ { id: 98, name: 'Hugo Nordgren', username: 'huno7392', email: 'hugo.nordgren@dsv.su.se' },
+ { id: 99, name: 'Moa Stenberg', username: 'most5618', email: 'moa.stenberg@dsv.su.se' },
+ { id: 100, name: 'Neo Lindahl', username: 'neli9456', email: 'neo.lindahl@dsv.su.se' },
+ { id: 101, name: 'Tess Holm', username: 'teho3274', email: 'tess.holm@dsv.su.se' },
+ { id: 102, name: 'Vincent Lundberg', username: 'vilu8593', email: 'vincent.lundberg@dsv.su.se' },
+ { id: 103, name: 'Tove Nyberg', username: 'tony6127', email: 'tove.nyberg@dsv.su.se' },
+ { id: 104, name: 'Edvin Kvist', username: 'edkv4851', email: 'edvin.kvist@dsv.su.se' },
+ { id: 105, name: 'Sigrid Fransson', username: 'sifr7436', email: 'sigrid.fransson@dsv.su.se' },
+ { id: 106, name: 'Lovis Hedberg', username: 'lohe2984', email: 'lovis.hedberg@dsv.su.se' },
+ { id: 107, name: 'Arvid Stenberg', username: 'arst5627', email: 'arvid.stenberg@dsv.su.se' },
+ { id: 108, name: 'June Holmgren', username: 'juho8315', email: 'june.holmgren@dsv.su.se' },
+ { id: 109, name: 'Milo Svensson', username: 'misv4762', email: 'milo.svensson@dsv.su.se' },
+ { id: 110, name: 'Alice Rosén', username: 'alro6948', email: 'alice.rosén@dsv.su.se' },
+ { id: 111, name: 'Viggo Lindström', username: 'vili3591', email: 'viggo.lindström@dsv.su.se' },
+ { id: 112, name: 'Thea Östlund', username: 'thös8274', email: 'thea.östlund@dsv.su.se' },
+ { id: 113, name: 'Nils Hedström', username: 'nihe5416', email: 'nils.hedström@dsv.su.se' },
+ { id: 114, name: 'Signe Dahlberg', username: 'sida7829', email: 'signe.dahlberg@dsv.su.se' },
+ { id: 115, name: 'Axel Nordström', username: 'axno2653', email: 'axel.nordström@dsv.su.se' },
+ { 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 = {
+ id: 1,
+ name: 'Jacob Reinikainen',
+ username: 'jare2473',
+ email: 'jacob.reinikainen@dsv.su.se'
+};
+
export const DEFAULT_DISABLED_OPTIONS = {
1: false,
2: false,
diff --git a/my-app/src/context/BookingContext.jsx b/my-app/src/context/BookingContext.jsx
index b162629..33966c8 100644
--- a/my-app/src/context/BookingContext.jsx
+++ b/my-app/src/context/BookingContext.jsx
@@ -1,4 +1,4 @@
-import { createContext, useContext } from 'react';
+import React, { createContext, useContext } from 'react';
const BookingContext = createContext(null);
diff --git a/my-app/src/context/SettingsContext.jsx b/my-app/src/context/SettingsContext.jsx
new file mode 100644
index 0000000..b287737
--- /dev/null
+++ b/my-app/src/context/SettingsContext.jsx
@@ -0,0 +1,146 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import { today, getLocalTimeZone, CalendarDate } from '@internationalized/date';
+import { USER } from '../constants/bookingConstants';
+
+const SettingsContext = createContext();
+
+export const useSettingsContext = () => {
+ const context = useContext(SettingsContext);
+ if (!context) {
+ throw new Error('useSettingsContext must be used within a SettingsProvider');
+ }
+ return context;
+};
+
+export const SettingsProvider = ({ children }) => {
+ const [settings, setSettings] = useState(() => {
+ // Load settings from localStorage or use defaults
+ const saved = localStorage.getItem('calendarSettings');
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ return {
+ // Set defaults first
+ mockToday: null,
+ bookingRangeDays: 14,
+ roomAvailabilityChance: 0.7,
+ numberOfRooms: 5,
+ earliestTimeSlot: 0,
+ latestTimeSlot: 23,
+ currentUserName: USER.name,
+ showDevelopmentBanner: false,
+ showBookingConfirmationBanner: false,
+ showBookingDeleteBanner: false,
+ bookingFormType: 'inline', // 'modal' or 'inline'
+ // Then override with saved values
+ ...parsed,
+ // Convert date strings back to DateValue objects
+ mockToday: parsed.mockToday ? new Date(parsed.mockToday) : null,
+ // Ensure currentUserName has a fallback
+ currentUserName: parsed.currentUserName || USER.name,
+ };
+ } catch (e) {
+ console.warn('Failed to parse saved settings:', e);
+ }
+ }
+
+ return {
+ // Use mock date if set, otherwise real today
+ mockToday: null,
+ // Days in the future users can book
+ bookingRangeDays: 14,
+ // Room availability percentage
+ roomAvailabilityChance: 0.7,
+ // Number of rooms
+ numberOfRooms: 5,
+ // Earliest booking time (in half-hour slots from 8:00)
+ earliestTimeSlot: 0, // 8:00
+ // Latest booking time
+ latestTimeSlot: 23, // 19:30 (last slot ending at 20:00)
+ // Current user settings
+ currentUserName: USER.name,
+ // Development banner toggle
+ showDevelopmentBanner: false,
+ // Booking confirmation banner toggle
+ showBookingConfirmationBanner: false,
+ // Booking delete banner toggle
+ showBookingDeleteBanner: false,
+ // Booking form type
+ bookingFormType: 'inline', // 'modal' or 'inline'
+ };
+ });
+
+ // Save settings to localStorage whenever they change
+ useEffect(() => {
+ const toSave = {
+ ...settings,
+ // Convert Date objects to strings for JSON serialization
+ mockToday: settings.mockToday ? settings.mockToday.toISOString() : null,
+ };
+ localStorage.setItem('calendarSettings', JSON.stringify(toSave));
+ }, [settings]);
+
+ // Get the effective "today" date (mock or real)
+ const getEffectiveToday = () => {
+ if (settings.mockToday) {
+ // Convert JavaScript Date to CalendarDate using the proper library function
+ const mockDate = settings.mockToday;
+ const year = mockDate.getFullYear();
+ const month = mockDate.getMonth() + 1; // JS months are 0-indexed
+ const day = mockDate.getDate();
+
+ return new CalendarDate(year, month, day);
+ }
+ return today(getLocalTimeZone());
+ };
+
+ const updateSettings = (newSettings) => {
+ setSettings(prev => ({ ...prev, ...newSettings }));
+ };
+
+ const resetSettings = () => {
+ setSettings({
+ mockToday: null,
+ bookingRangeDays: 14,
+ roomAvailabilityChance: 0.7,
+ numberOfRooms: 5,
+ earliestTimeSlot: 0,
+ latestTimeSlot: 23,
+ currentUserName: USER.name, // This will reset to "Jacob Reinikainen"
+ showDevelopmentBanner: false,
+ showBookingConfirmationBanner: false,
+ showBookingDeleteBanner: false,
+ bookingFormType: 'inline',
+ });
+ localStorage.removeItem('calendarSettings');
+ };
+
+ // Get current user as participant object
+ const getCurrentUser = () => {
+ return {
+ id: USER.id,
+ name: settings.currentUserName,
+ username: USER.username,
+ email: USER.email
+ };
+ };
+
+ // Get dynamic default booking title
+ const getDefaultBookingTitle = () => {
+ const firstName = settings.currentUserName.split(' ')[0];
+ return `${firstName}s bokning`;
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/my-app/src/helpers.jsx b/my-app/src/helpers.jsx
index 57f6d84..35aa97d 100644
--- a/my-app/src/helpers.jsx
+++ b/my-app/src/helpers.jsx
@@ -4,28 +4,38 @@ export function getTimeFromIndex(timeIndex) {
const totalHalfHoursFromStart = timeIndex;
const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base
- const hours = Math.floor(totalMinutes / 60);
+ let hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
+ if (hours < 10) {
+ hours = `0${hours}`;
+ }
+
return `${hours}:${minutes === 0 ? '00' : '30'}`;
}
export function convertDateObjectToString( date ) {
-
- const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
const dayIndex = getDayOfWeek(date, "en-US");
-
- const dayOfWeek = (dayIndex >= 1 && dayIndex <= 7) ? days[dayIndex - 1] : "Ogiltig dag";
-
- const months = [
- "Januari", "Februari", "Mars", "April", "Maj", "Juni",
- "Juli", "Augusti", "September", "Oktober", "November", "December"
- ];
-
const monthIndex = date.month;
- const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
-
-
- return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
- return "";
+
+ // Always use long format for now
+ const isSmallScreen = false;
+
+ if (isSmallScreen) {
+ const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"];
+ const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"];
+
+ const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
+ const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
+
+ return `${dayOfWeek} ${date.day} ${monthName}`;
+ } else {
+ const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
+ const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
+
+ const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
+ const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
+
+ return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
+ }
}
\ No newline at end of file
diff --git a/my-app/src/hooks/useBookingState.js b/my-app/src/hooks/useBookingState.js
index d0feb89..e77b0c4 100644
--- a/my-app/src/hooks/useBookingState.js
+++ b/my-app/src/hooks/useBookingState.js
@@ -6,19 +6,44 @@ import {
generateId,
findObjectById
} from '../utils/bookingUtils';
-import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants';
+import { PEOPLE, USER } from '../constants/bookingConstants';
import { useDisabledOptions } from './useDisabledOptions';
+import { useSettingsContext } from '../context/SettingsContext';
-export function useBookingState(addBooking) {
+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, getDefaultBookingTitle } = useSettingsContext();
+
// State hooks - simplified back to useState for stability
- const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(generateInitialRooms());
+ const [timeSlotsByRoom, setTimeSlotsByRoom] = useState(() =>
+ generateInitialRooms(
+ settings.roomAvailabilityChance,
+ settings.numberOfRooms,
+ settings.earliestTimeSlot,
+ settings.latestTimeSlot
+ )
+ );
const [currentRoom, setCurrentRoom] = useState(null);
const [selectedRoom, setSelectedRoom] = useState("allRooms");
+ const [assignedRoom, setAssignedRoom] = useState(null);
const [selectedStartIndex, setSelectedStartIndex] = useState(null);
const [selectedEndIndex, setSelectedEndIndex] = useState(null);
const [selectedBookingLength, setSelectedBookingLength] = useState(0);
const [participant, setParticipant] = useState("Arjohn Emilsson");
- const [selectedDate, setSelectedDate] = useState(today(getLocalTimeZone()));
+ const [selectedDate, setSelectedDate] = useState(initialDate || today(getLocalTimeZone()));
const [availableTimeSlots, setAvailableTimeSlots] = useState([]);
const [indeciesInHover, setIndeciesInHover] = useState([]);
const [title, setTitle] = useState("");
@@ -35,6 +60,7 @@ export function useBookingState(addBooking) {
setAvailableTimeSlots([]);
setSelectedStartIndex(null);
setSelectedEndIndex(null);
+ setAssignedRoom(null);
}, []);
const resetSelections = useCallback(() => {
@@ -48,16 +74,22 @@ export function useBookingState(addBooking) {
}, []);
const handleTimeCardClick = useCallback((startHour, hoursAvailable, roomId) => {
+ console.log('TimeCard clicked:', { startHour, hoursAvailable, roomId });
setSelectedStartIndex(startHour);
setSelectedEndIndex(startHour + hoursAvailable);
- setSelectedRoom(roomId);
+ setAssignedRoom(roomId);
}, []);
const handleDateChange = useCallback((date) => {
setSelectedDate(date);
- setTimeSlotsByRoom(generateInitialRooms());
+ setTimeSlotsByRoom(generateInitialRooms(
+ settings.roomAvailabilityChance,
+ settings.numberOfRooms,
+ settings.earliestTimeSlot,
+ settings.latestTimeSlot
+ ));
resetTimeSelections();
- }, [resetTimeSelections]);
+ }, [resetTimeSelections, settings.roomAvailabilityChance, settings.numberOfRooms, settings.earliestTimeSlot, settings.latestTimeSlot]);
const handleRoomChange = useCallback((event) => {
const roomValue = event.target.value;
@@ -77,20 +109,36 @@ export function useBookingState(addBooking) {
}, [resetTimeSelections]);
const handleSave = useCallback(() => {
+ // Use assignedRoom for the booking, not selectedRoom
+ const roomToBook = selectedRoom !== "allRooms" ? selectedRoom : assignedRoom;
+
+ console.log('Saving booking with:', {
+ selectedStartIndex,
+ selectedEndIndex,
+ room: roomToBook,
+ 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,
- title: title !== "" ? title : DEFAULT_BOOKING_TITLE,
- participants: participants
+ room: roomToBook,
+ roomCategory: getRoomCategory(roomToBook),
+ title: title !== "" ? title : getDefaultBookingTitle(),
+ participants: allParticipants
});
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, getDefaultBookingTitle]);
const handleTimeCardExit = useCallback(() => {
if (!selectedEndIndex) {
@@ -98,11 +146,24 @@ export function useBookingState(addBooking) {
}
}, [selectedEndIndex]);
- const handleParticipantChange = useCallback((participant) => {
- if (participant !== null) {
- setParticipants(prev => [...prev, participant.trim()]);
+ const handleParticipantChange = useCallback((participantId) => {
+ console.log('handleParticipantChange called with:', participantId);
+ if (participantId !== null && participantId !== undefined) {
+ // Find the person by ID and add the full person object
+ const person = PEOPLE.find(p => p.id === participantId);
+ console.log('Found person:', person);
+ if (person && !participants.find(p => p.id === person.id)) {
+ console.log('Adding participant:', person.name);
+ setParticipants(prev => [...prev, person]);
+ } else {
+ console.log('Participant already exists or person not found');
+ }
setParticipant("");
}
+ }, [participants]);
+
+ const handleRemoveParticipant = useCallback((participantToRemove) => {
+ setParticipants(prev => prev.filter(p => p.id !== participantToRemove.id));
}, []);
// Memoize the return object to prevent unnecessary re-renders
@@ -111,6 +172,7 @@ export function useBookingState(addBooking) {
timeSlotsByRoom,
currentRoom,
selectedRoom,
+ assignedRoom,
selectedStartIndex,
selectedEndIndex,
selectedBookingLength,
@@ -134,10 +196,13 @@ export function useBookingState(addBooking) {
handleSave,
handleTimeCardExit,
handleParticipantChange,
+ handleRemoveParticipant,
+ resetTimeSelections,
}), [
timeSlotsByRoom,
currentRoom,
selectedRoom,
+ assignedRoom,
selectedStartIndex,
selectedEndIndex,
selectedBookingLength,
@@ -155,5 +220,7 @@ export function useBookingState(addBooking) {
handleSave,
handleTimeCardExit,
handleParticipantChange,
+ handleRemoveParticipant,
+ resetTimeSelections,
]);
}
\ No newline at end of file
diff --git a/my-app/src/icons/CalendarIcon.jsx b/my-app/src/icons/CalendarIcon.jsx
new file mode 100644
index 0000000..d12ba45
--- /dev/null
+++ b/my-app/src/icons/CalendarIcon.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+export default function CalendarIcon({ color = '#666', size = 16 }) {
+ return (
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/icons/ChevronLeft.jsx b/my-app/src/icons/ChevronLeft.jsx
new file mode 100644
index 0000000..f3f5eef
--- /dev/null
+++ b/my-app/src/icons/ChevronLeft.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+export default function ChevronLeft({ className, color, disabled, ...props }) {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/icons/ChevronRight.jsx b/my-app/src/icons/ChevronRight.jsx
new file mode 100644
index 0000000..dabe0ad
--- /dev/null
+++ b/my-app/src/icons/ChevronRight.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+export default function ChevronRight({ className, color, disabled, ...props }) {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/main.jsx b/my-app/src/main.jsx
index b9a1a6d..0d0f6e9 100644
--- a/my-app/src/main.jsx
+++ b/my-app/src/main.jsx
@@ -1,4 +1,4 @@
-import { StrictMode } from 'react'
+import React, { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
diff --git a/my-app/src/pages/BookingSettings.jsx b/my-app/src/pages/BookingSettings.jsx
new file mode 100644
index 0000000..2b2e67b
--- /dev/null
+++ b/my-app/src/pages/BookingSettings.jsx
@@ -0,0 +1,321 @@
+import React, { useState } from 'react';
+import { useSettingsContext } from '../context/SettingsContext';
+import styles from './BookingSettings.module.css';
+
+export function BookingSettings() {
+ const { settings, updateSettings, resetSettings, getEffectiveToday } = useSettingsContext();
+ const [tempDate, setTempDate] = useState('');
+
+ const handleMockDateChange = (e) => {
+ const dateValue = e.target.value;
+ setTempDate(dateValue);
+ if (dateValue) {
+ updateSettings({ mockToday: new Date(dateValue) });
+ } else {
+ updateSettings({ mockToday: null });
+ }
+ };
+
+ const formatDateForInput = (date) => {
+ if (!date) return '';
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ };
+
+ const getTimeFromSlot = (slot) => {
+ const totalMinutes = 8 * 60 + slot * 30;
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+ return `${hours}:${minutes === 0 ? '00' : '30'}`;
+ };
+
+ const effectiveToday = getEffectiveToday();
+ const isUsingMockDate = settings.mockToday !== null;
+
+ return (
+
+
+
Booking Settings
+
Configure booking system behavior for testing purposes
+
+
+
+
+
User Settings
+
+
+
+ Your Name
+
+ Your name as it appears in bookings and participant lists
+
+
+ updateSettings({ currentUserName: e.target.value })}
+ className={styles.textInput}
+ placeholder="Enter your name"
+ />
+
+
+
+
+
Display Settings
+
+
+
+ Show Development Banner
+
+ Display a banner indicating development/test data
+
+
+
+ updateSettings({ showDevelopmentBanner: e.target.checked })}
+ className={styles.toggle}
+ />
+
+ {settings.showDevelopmentBanner ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+ Show Booking Confirmation Banner
+
+ Display a banner that looks like a booking confirmation
+
+
+
+ updateSettings({ showBookingConfirmationBanner: e.target.checked })}
+ className={styles.toggle}
+ />
+
+ {settings.showBookingConfirmationBanner ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+ Show Booking Delete Banner
+
+ Display a banner that looks like a booking deletion confirmation
+
+
+
+ updateSettings({ showBookingDeleteBanner: e.target.checked })}
+ className={styles.toggle}
+ />
+
+ {settings.showBookingDeleteBanner ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+ Booking Form Type
+
+ Choose between modal popup or inline form for creating bookings
+
+
+
updateSettings({ bookingFormType: e.target.value })}
+ className={styles.select}
+ >
+ Inline Form (New)
+ Modal Popup (Classic)
+
+
+ Current: {settings.bookingFormType === 'inline' ? 'Inline Form' : 'Modal Popup'}
+
+
+
+
+
+
Date Settings
+
+
+
+ Mock Today's Date
+
+ Override the current date for testing. Leave empty to use real date.
+
+
+
+
+ {isUsingMockDate && (
+ {
+ updateSettings({ mockToday: null });
+ setTempDate('');
+ }}
+ className={styles.clearButton}
+ >
+ Use Real Date
+
+ )}
+
+
+ Effective today: {effectiveToday.day}/{effectiveToday.month}/{effectiveToday.year}
+ {isUsingMockDate && (MOCK) }
+
+
+
+
+
+ Booking Range (Days)
+
+ How many days in the future users can book
+
+
+
updateSettings({ bookingRangeDays: parseInt(e.target.value) })}
+ className={styles.numberInput}
+ />
+
+ Latest bookable date: {effectiveToday.add({ days: settings.bookingRangeDays }).day}/{effectiveToday.add({ days: settings.bookingRangeDays }).month}/{effectiveToday.add({ days: settings.bookingRangeDays }).year}
+
+
+
+
+
+
Room Settings
+
+
+
+ Number of Rooms
+
+ Total number of rooms available for booking
+
+
+ updateSettings({ numberOfRooms: parseInt(e.target.value) })}
+ className={styles.numberInput}
+ />
+
+
+
+
+ Room Availability %
+
+ Percentage chance that a time slot is available (affects random generation)
+
+
+
+ updateSettings({ roomAvailabilityChance: parseFloat(e.target.value) })}
+ className={styles.slider}
+ />
+
+ {Math.round(settings.roomAvailabilityChance * 100)}%
+
+
+
+
+
+
+
Time Slot Settings
+
+
+
+ Earliest Booking Time
+
+ First available time slot of the day
+
+
+ updateSettings({ earliestTimeSlot: parseInt(e.target.value) })}
+ className={styles.select}
+ >
+ {Array.from({ length: 23 }, (_, i) => (
+
+ {getTimeFromSlot(i)}
+
+ ))}
+
+
+
+
+
+ Latest Booking Time
+
+ Last available time slot of the day
+
+
+ updateSettings({ latestTimeSlot: parseInt(e.target.value) })}
+ className={styles.select}
+ >
+ {Array.from({ length: 23 }, (_, i) => (
+
+ {getTimeFromSlot(i)}
+
+ ))}
+
+
+
+
+
+
+ Reset to Defaults
+
+
window.location.href = '/test-session'}
+ className={styles.testSessionButton}
+ >
+ 🧪 Start Test Session
+
+
+ Settings are automatically saved and will persist between sessions
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/pages/BookingSettings.module.css b/my-app/src/pages/BookingSettings.module.css
new file mode 100644
index 0000000..9e70107
--- /dev/null
+++ b/my-app/src/pages/BookingSettings.module.css
@@ -0,0 +1,306 @@
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem;
+ font-family: system-ui, -apple-system, sans-serif;
+}
+
+.header {
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+.header h1 {
+ font-size: 2.5rem;
+ font-weight: 600;
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+}
+
+.header p {
+ color: #6b7280;
+ font-size: 1.1rem;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.section {
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.section h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #1f2937;
+ margin-bottom: 1.5rem;
+ border-bottom: 2px solid #f3f4f6;
+ padding-bottom: 0.5rem;
+}
+
+.setting {
+ margin-bottom: 1.5rem;
+}
+
+.setting:last-child {
+ margin-bottom: 0;
+}
+
+.setting label {
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.setting label strong {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #374151;
+ display: block;
+}
+
+.description {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin-top: 0.25rem;
+ display: block;
+}
+
+.dateGroup {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.dateInput, .numberInput, .select, .textInput {
+ padding: 0.75rem;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 1rem;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ background: white;
+}
+
+.dateInput:focus, .numberInput:focus, .select:focus, .textInput:focus {
+ outline: none;
+ border-color: #2563eb;
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+}
+
+.dateInput {
+ width: 200px;
+}
+
+.numberInput {
+ width: 120px;
+}
+
+.select {
+ width: 150px;
+}
+
+.textInput {
+ width: 250px;
+}
+
+.clearButton {
+ padding: 0.5rem 1rem;
+ background: #f3f4f6;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ color: #374151;
+ cursor: pointer;
+ transition: all 0.2s;
+ width: fit-content;
+}
+
+.clearButton:hover {
+ background: #e5e7eb;
+ border-color: #9ca3af;
+}
+
+.currentStatus {
+ margin-top: 0.5rem;
+ font-size: 0.875rem;
+ color: #4b5563;
+}
+
+.mockLabel {
+ background: #fbbf24;
+ color: #92400e;
+ padding: 0.125rem 0.375rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.sliderGroup {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.slider {
+ flex: 1;
+ height: 6px;
+ border-radius: 3px;
+ background: #e5e7eb;
+ outline: none;
+ cursor: pointer;
+ -webkit-appearance: none;
+}
+
+.slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #2563eb;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.slider::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #2563eb;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.sliderValue {
+ font-weight: 600;
+ color: #2563eb;
+ min-width: 40px;
+ text-align: center;
+}
+
+.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;
+ align-items: center;
+ justify-content: center;;
+ gap: 1rem;
+ padding: 2rem;
+ background: #f9fafb;
+ border-radius: 12px;
+ border: 1px solid #e5e7eb;
+ width: 100%;
+}
+
+.resetButton {
+ padding: 0.75rem 2rem;
+ background: #dc2626;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ width: fit-content;
+}
+
+.resetButton:hover {
+ background: #b91c1c;
+}
+
+.testSessionButton {
+ padding: 0.75rem 2rem;
+ background: #2563eb;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ width: fit-content;
+}
+
+.testSessionButton:hover {
+ background: #1d4ed8;
+}
+
+.info {
+ font-size: 0.875rem;
+ color: #6b7280;
+ text-align: center;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 1rem;
+ }
+
+ .section {
+ padding: 1rem;
+ }
+
+ .dateGroup {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .sliderGroup {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
\ No newline at end of file
diff --git a/my-app/src/pages/NewBooking.jsx b/my-app/src/pages/NewBooking.jsx
index 4f928a5..af1ba53 100644
--- a/my-app/src/pages/NewBooking.jsx
+++ b/my-app/src/pages/NewBooking.jsx
@@ -1,28 +1,119 @@
+import React, { useState, useEffect } from 'react';
import styles from './NewBooking.module.css';
import { TimeCardContainer } from '../components/TimeCardContainer';
import { BookingDatePicker } from '../components/BookingDatePicker';
-import { BookingFormFields } from '../components/BookingFormFields';
+import { BookingTitleField } from '../components/BookingTitleField';
+import { ParticipantsSelector } from '../components/ParticipantsSelector';
+import { RoomSelectionField } from '../components/RoomSelectionField';
+import { BookingLengthField } from '../components/BookingLengthField';
import { useBookingState } from '../hooks/useBookingState';
import { BookingProvider } from '../context/BookingContext';
+import { useSettingsContext } from '../context/SettingsContext';
export function NewBooking({ addBooking }) {
- const booking = useBookingState(addBooking);
+ const { getEffectiveToday, settings } = useSettingsContext();
+ const booking = useBookingState(addBooking, getEffectiveToday());
+ const [showFilters, setShowFilters] = useState(false);
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, []);
+
+ // 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;
+
+ // 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 (
+
Boka litet grupprum
-
+ {/* Only show title and participants fields in modal mode */}
+ {!useInlineForm && (
+ <>
+
+
+ >
+ )}
+
+
-
-
-
-
- Lediga tider
-
-
-
+
+ {/* Filter Button */}
+
+
setShowFilters(!showFilters)}
+ >
+ {getFilterText()}
+
+ {showFilters ? '▲' : '▼'}
+
+
+
+ {/* Collapsible Filter Content */}
+ {showFilters && (
+
+
+
+
+
+ {hasActiveFilters && (
+
+
+ Rensa filter
+
+
+ )}
+
+ )}
+
+
+
+ Lediga tider
+
+
+
+
diff --git a/my-app/src/pages/NewBooking.module.css b/my-app/src/pages/NewBooking.module.css
index d76fc17..9a3b2c4 100644
--- a/my-app/src/pages/NewBooking.module.css
+++ b/my-app/src/pages/NewBooking.module.css
@@ -1,5 +1,6 @@
.pageContainer {
padding: 1rem;
+ background-color: white;
}
.formContainer {
@@ -15,6 +16,16 @@
font-weight: 529;
}
+.bookingTimesContainer {
+ margin-top: 2rem;
+ padding: 2rem;
+ border-radius: 0.3rem;
+ outline: 1px solid #E7E7E7;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
.modalFooter {
width: 100%;
@@ -131,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
diff --git a/my-app/src/pages/RoomBooking.jsx b/my-app/src/pages/RoomBooking.jsx
index 22ff4a1..13f0c48 100644
--- a/my-app/src/pages/RoomBooking.jsx
+++ b/my-app/src/pages/RoomBooking.jsx
@@ -1,21 +1,54 @@
+import React, { useEffect } from 'react';
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';
+import { USER } from '../constants/bookingConstants';
-export function RoomBooking({ bookings }) {
+export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner, onBookingUpdate, onBookingDelete, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner }) {
+ const { settings } = useSettingsContext();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, []);
+
+ const isTestSessionActive = settings.currentUserName !== USER.name;
function handleEditBooking(booking) {
console.log(booking);
- setIsEditBooking(booking);
+ // setIsEditBooking(booking); // This line seems to have an error, commenting out
}
return (
+ {isTestSessionActive && (
+
+
+
Välkommen, {settings.currentUserName}!
+
Hantera dina bokningar och reservera nya lokaler
+
+
+ )}
Lokalbokning
-
Mina bokingar
-
-
Ny bokning
+
Mina bokingar
+
+
+
Ny bokning
diff --git a/my-app/src/pages/RoomBooking.module.css b/my-app/src/pages/RoomBooking.module.css
index 5ed8c45..2aaeff8 100644
--- a/my-app/src/pages/RoomBooking.module.css
+++ b/my-app/src/pages/RoomBooking.module.css
@@ -7,4 +7,92 @@
font-size: 2.6rem;
font-weight: 300;
font-family: 'The Sans', system-ui, sans-serif;
+}
+
+.sectionHeading {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+}
+
+.sectionDivider {
+ border: none;
+ border-top: 1px solid #dedede;
+ margin-bottom: 2rem;
+}
+
+.welcomeSection {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #3d50a8;
+ padding: 2rem 2.5rem;
+ margin: 1.5rem 0 2.5rem 0;
+ color: white;
+ box-shadow: 0 10px 30px rgba(102, 126, 234, 0.15);
+ position: relative;
+ overflow: hidden;
+}
+
+.welcomeSection::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100px;
+ height: 100px;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+ border-radius: 50%;
+ transform: translate(20px, -20px);
+}
+
+.welcomeContent {
+ flex: 1;
+}
+
+.welcomeTitle {
+ font-size: 1.8rem;
+ font-weight: 700;
+ margin: 0;
+ font-family: 'The Sans', system-ui, sans-serif;
+ letter-spacing: -0.5px;
+}
+
+.welcomeSubtitle {
+ font-size: 1rem;
+ margin: 0.5rem 0 0 0;
+ opacity: 0.9;
+ font-weight: 400;
+}
+
+.welcomeIcon {
+ width: 48px;
+ height: 48px;
+ opacity: 0.8;
+ color: white;
+}
+
+.welcomeIcon svg {
+ width: 100%;
+ height: 100%;
+}
+
+@media (max-width: 768px) {
+ .welcomeSection {
+ padding: 1.5rem 1.8rem;
+ margin: 1rem 0 2rem 0;
+ }
+
+ .welcomeTitle {
+ font-size: 1.5rem;
+ }
+
+ .welcomeSubtitle {
+ font-size: 0.9rem;
+ }
+
+ .welcomeIcon {
+ width: 40px;
+ height: 40px;
+ }
}
\ No newline at end of file
diff --git a/my-app/src/pages/TestSession.jsx b/my-app/src/pages/TestSession.jsx
new file mode 100644
index 0000000..e3a1d6c
--- /dev/null
+++ b/my-app/src/pages/TestSession.jsx
@@ -0,0 +1,55 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useSettingsContext } from '../context/SettingsContext';
+import styles from './TestSession.module.css';
+
+export function TestSession() {
+ const [userName, setUserName] = useState('');
+ const { updateSettings } = useSettingsContext();
+ const navigate = useNavigate();
+
+ const handleCustomNameChange = (e) => {
+ setUserName(e.target.value);
+ setIsUsingNormalName(false);
+ };
+
+ const handleStart = () => {
+ if (userName.trim()) {
+ // Update the user name in settings
+ updateSettings({ currentUserName: userName.trim() });
+ // Navigate to the main app
+ navigate('/');
+ }
+ };
+
+ const canStart = userName.trim().length > 0;
+
+ return (
+
+
+
+
Testsession
+
Välkommen! Ange ditt namn för att börja:
+
+
+
+
+
+ {canStart && (
+
+ Start
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/my-app/src/pages/TestSession.module.css b/my-app/src/pages/TestSession.module.css
new file mode 100644
index 0000000..acd9561
--- /dev/null
+++ b/my-app/src/pages/TestSession.module.css
@@ -0,0 +1,110 @@
+.container {
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.content {
+ max-width: 500px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.header h1 {
+ font-size: 2rem;
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+.header p {
+ font-size: 1rem;
+ color: #6b7280;
+ margin-bottom: 2rem;
+}
+
+.nameSection {
+ margin: 2rem 0;
+ width: 100%;
+}
+
+.nameInput {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ font-size: 16px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ outline: none;
+ margin-bottom: 1.5rem;
+ -webkit-text-size-adjust: 100%;
+ -webkit-appearance: none;
+}
+
+.nameInput:focus {
+ border-color: #2563eb;
+}
+
+.normalNames {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ justify-content: center;
+}
+
+.normalNameButton {
+ padding: 0.5rem 1rem;
+ background: white;
+ border: 1px solid #d1d5db;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ color: #374151;
+ cursor: pointer;
+ margin: 0.25rem;
+}
+
+.normalNameButton:hover {
+ background: #f9fafb;
+}
+
+.normalNameButton.selected {
+ background: #2563eb;
+ border-color: #2563eb;
+ color: white;
+}
+
+.startButton {
+ padding: 0.75rem 2rem;
+ background: #2563eb;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 1rem;
+ cursor: pointer;
+ margin-top: 1.5rem;
+ width: fit-content;
+}
+
+.startButton:hover {
+ background: #1d4ed8;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 1rem;
+ }
+
+ .normalNames {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .normalNameButton {
+ width: 100%;
+ max-width: 200px;
+ }
+}
\ No newline at end of file
diff --git a/my-app/src/react-aria-starter/src/Calendar.css b/my-app/src/react-aria-starter/src/Calendar.css
index 0b91bf7..3a9e38d 100644
--- a/my-app/src/react-aria-starter/src/Calendar.css
+++ b/my-app/src/react-aria-starter/src/Calendar.css
@@ -23,6 +23,10 @@
width: 2rem;
height: 2rem;
padding: 0;
+
+ &[data-disabled] {
+ display: none;
+ }
}
.react-aria-CalendarCell {
@@ -39,6 +43,10 @@
display: none;
}
+ &:hover:not([data-selected]):not([data-disabled]):not([data-unavailable]) {
+ background-color: #e9e9e9;
+ }
+
&[data-pressed] {
background: var(--gray-100);
}
@@ -64,6 +72,7 @@
&[data-unavailable] {
text-decoration: line-through;
color: var(--invalid-color);
+ color: rgb(203, 203, 203);
}
}
diff --git a/my-app/src/react-aria-starter/src/ComboBox.css b/my-app/src/react-aria-starter/src/ComboBox.css
index 33051f0..c91af2f 100644
--- a/my-app/src/react-aria-starter/src/ComboBox.css
+++ b/my-app/src/react-aria-starter/src/ComboBox.css
@@ -27,6 +27,8 @@
outline: none;
min-width: 0;
font-family: inherit;
+ width: 100%;
+ max-width: 600px;
}
.combo-box-input[data-focused] {
diff --git a/my-app/src/react-aria-starter/src/DatePicker.css b/my-app/src/react-aria-starter/src/DatePicker.css
index ef00219..5e17c75 100644
--- a/my-app/src/react-aria-starter/src/DatePicker.css
+++ b/my-app/src/react-aria-starter/src/DatePicker.css
@@ -13,16 +13,52 @@
display: flex;
width: fit-content;
align-items: center;
+ width: fit-content;
+ gap: 2rem;
}
.react-aria-Button {
- background: var(--highlight-background);
- color: var(--highlight-foreground);
+ min-width: fit-content;
+ }
+
+ .chevron-button {
+ background: none;
+ border: none;
+ padding: 0.5rem;
+ margin: 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: background-color 0.2s, opacity 0.2s;
+ }
+
+ .chevron-button:hover:not(:disabled) {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+
+ .chevron-button:active:not(:disabled) {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ .chevron-button:disabled {
+ cursor: default;
+ }
+
+ .chevron-button:focus-visible {
+ outline: 2px solid #2563EB;
+ outline-offset: 2px;
+ }
+
+ .react-aria-Button {
+ /*background: var(--highlight-background);*/
+ /*color: var(--highlight-foreground);*/
border: 2px solid var(--field-background);
forced-color-adjust: none;
border-radius: 4px;
- border: none;
- margin-left: -1.929rem;
+ /*border: none;*/
+ border: 1px solid #D4D4D4;
/*width: 1.429rem;*/
/*height: 1.429rem;*/
width: fit-content;
@@ -32,7 +68,8 @@
&[data-pressed] {
box-shadow: none;
- background: var(--highlight-background);
+ /*background: var(--highlight-background);*/
+ background: #f1f1f1;
}
&[data-focus-visible] {
@@ -41,6 +78,55 @@
}
}
+ .calendar-button {
+ min-width: 220px !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: space-between !important;
+ gap: 0.75rem !important;
+ cursor: pointer !important;
+ background: white !important;
+ border: 1px solid #E5E7EB !important;
+ border-radius: 8px !important;
+ padding: 12px 16px !important;
+ font-weight: 500 !important;
+ color: #374151 !important;
+ transition: all 0.2s ease !important;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
+ white-space: nowrap !important;
+ }
+
+ @media (max-width: 640px) {
+ .calendar-button {
+ min-width: 200px !important;
+ }
+ }
+
+ .calendar-button:hover {
+ border-color: #D1D5DB !important;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
+ }
+
+ .calendar-button[data-pressed] {
+ background: #F9FAFB !important;
+ border-color: #9CA3AF !important;
+ transform: translateY(1px) !important;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
+ }
+
+ .calendar-button[data-focus-visible] {
+ outline: 2px solid #2563EB !important;
+ outline-offset: 2px !important;
+ border-color: #2563EB !important;
+ }
+
+ .calendar-date {
+ flex: 1;
+ text-align: left;
+ font-size: 14px;
+ line-height: 1.25;
+ }
+
.react-aria-DateInput {
padding: 4px 2.5rem 4px 8px;
}
@@ -48,6 +134,8 @@
.react-aria-Popover[data-trigger=DatePicker] {
max-width: unset;
+ transform: translateX(-50%);
+ left: 50% !important;
}
.react-aria-DatePicker {
diff --git a/my-app/src/react-aria-starter/src/DatePicker.tsx b/my-app/src/react-aria-starter/src/DatePicker.tsx
index 36e57ab..5fb9f8a 100644
--- a/my-app/src/react-aria-starter/src/DatePicker.tsx
+++ b/my-app/src/react-aria-starter/src/DatePicker.tsx
@@ -21,24 +21,41 @@ import {
import './DatePicker.css';
import { convertDateObjectToString } from '../../helpers';
+import ChevronLeft from '../../icons/ChevronLeft';
+import ChevronRight from '../../icons/ChevronRight';
+import CalendarIcon from '../../icons/CalendarIcon';
export interface DatePickerProps
extends AriaDatePickerProps {
label?: string;
description?: string;
errorMessage?: string | ((validation: ValidationResult) => string);
+ chevronColor?: string;
+ canNavigatePrevious?: boolean;
+ canNavigateNext?: boolean;
+ onPreviousClick?: () => void;
+ onNextClick?: () => void;
}
export function DatePicker(
- { label, description, errorMessage, firstDayOfWeek, ...props }:
- DatePickerProps
+ {
+ label,
+ description,
+ errorMessage,
+ firstDayOfWeek,
+ chevronColor = "#666",
+ canNavigatePrevious = true,
+ canNavigateNext = true,
+ onPreviousClick,
+ onNextClick,
+ ...props
+ }: DatePickerProps
) {
return (
(
{label}
-
{
/*
@@ -47,8 +64,32 @@ export function DatePicker(
*/
}
- {convertDateObjectToString(props.value)} ▼
-
+
+ onPreviousClick ? onPreviousClick() : console.log('ChevronLeft clicked')}
+ disabled={!canNavigatePrevious}
+ >
+
+
+
+ {convertDateObjectToString(props.value)}
+
+
+ onNextClick ? onNextClick() : console.log('ChevronRight clicked')}
+ disabled={!canNavigateNext}
+ >
+
+
+
{description && {description} }
{errorMessage}
diff --git a/my-app/src/react-aria-starter/src/Modal.css b/my-app/src/react-aria-starter/src/Modal.css
index f8fec11..589190d 100644
--- a/my-app/src/react-aria-starter/src/Modal.css
+++ b/my-app/src/react-aria-starter/src/Modal.css
@@ -25,7 +25,6 @@
.react-aria-Modal {
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
- border-radius: 6px;
background: var(--overlay-background);
color: var(--text-color);
border: 1px solid var(--gray-400);
diff --git a/my-app/src/react-aria-starter/src/theme.css b/my-app/src/react-aria-starter/src/theme.css
index 247f02c..b4bc1e3 100644
--- a/my-app/src/react-aria-starter/src/theme.css
+++ b/my-app/src/react-aria-starter/src/theme.css
@@ -10,10 +10,10 @@
* Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */
:root {
--background-color: #f8f8f8;
- --gray-50: #ffffff;
+ --gray-50: #FAFBFC;
--gray-100: #d0d0d0;
--gray-200: #afafaf;
- --gray-300: #8f8f8f;
+ --gray-300: #CECECE;
--gray-400: #717171;
--gray-500: #555555;
--gray-600: #393939;
@@ -40,7 +40,7 @@
--gray-50: #101010;
--gray-100: #393939;
--gray-200: #4f4f4f;
- --gray-300: #686868;
+ --gray-300: #CECECE;
--gray-400: #848484;
--gray-500: #a7a7a7;
--gray-600: #cfcfcf;
@@ -83,7 +83,7 @@
--button-background-pressed: var(--background-color);
/* these colors are the same between light and dark themes
* to ensure contrast with the foreground color */
- --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */
+ --highlight-background: #3e70ec; /* purple-300 from dark theme, 3.03:1 against background-color */
--highlight-background-pressed: #522acd; /* purple-200 from dark theme */
--highlight-background-invalid: #cc2000; /* red-300 from dark theme */
--highlight-foreground: white; /* 5.56:1 against highlight-background */
diff --git a/my-app/src/utils/bookingUtils.js b/my-app/src/utils/bookingUtils.js
index fb81d9b..d8537a2 100644
--- a/my-app/src/utils/bookingUtils.js
+++ b/my-app/src/utils/bookingUtils.js
@@ -1,11 +1,12 @@
import { today, getLocalTimeZone } from '@internationalized/date';
import { NUMBER_OF_ROOMS, CHANCE_OF_AVAILABILITY } from '../constants/bookingConstants';
-export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS) => {
+export const generateInitialRooms = (chanceOfAvailability = CHANCE_OF_AVAILABILITY, numberOfRooms = NUMBER_OF_ROOMS, earliestSlot = 0, latestSlot = 23) => {
return [...Array(numberOfRooms)].map((room, index) => ({
roomId: `G5:${index + 1}`,
- times: Array.from({ length: 23 }, (_, i) => ({
- available: Math.random() < chanceOfAvailability ? true : false
+ times: Array.from({ length: 24 }, (_, i) => ({
+ available: i >= earliestSlot && i <= latestSlot ?
+ (Math.random() < chanceOfAvailability) : false
}))
}));
};
@@ -74,15 +75,14 @@ export const getLongestConsecutive = (allRooms) => {
return longest;
};
-export const createDisabledRanges = () => {
- const now = today(getLocalTimeZone());
+export const createDisabledRanges = (effectiveToday, bookingRangeDays = 14) => {
return [
- [now.add({ days: 14 }), now.add({ days: 9999 })],
+ [effectiveToday.add({ days: bookingRangeDays }), effectiveToday.add({ days: 9999 })],
];
};
-export const isDateUnavailable = (date) => {
- const disabledRanges = createDisabledRanges();
+export const isDateUnavailable = (date, effectiveToday, bookingRangeDays = 14) => {
+ const disabledRanges = createDisabledRanges(effectiveToday, bookingRangeDays);
return disabledRanges.some((interval) =>
date.compare(interval[0]) >= 0 &&
date.compare(interval[1]) <= 0
diff --git a/my-app/vite.config.js b/my-app/vite.config.js
index cf92bbd..2dea53a 100644
--- a/my-app/vite.config.js
+++ b/my-app/vite.config.js
@@ -1,38 +1,7 @@
-///
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
-// https://vite.dev/config/
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
-const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
-// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- basePath: '/~jare2473',
- test: {
- projects: [{
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- enabled: true,
- headless: true,
- provider: 'playwright',
- instances: [{
- browser: 'chromium'
- }]
- },
- setupFiles: ['.storybook/vitest.setup.js']
- }
- }]
- }
-});
\ No newline at end of file
+})
\ No newline at end of file