booking-flow-finalized-design kindaaaa #7

Merged
jare2473 merged 20 commits from booking-flow-finalized-design into main 2025-09-30 10:50:54 +02:00
8 changed files with 326 additions and 141 deletions
Showing only changes of commit 19e63bfe2f - Show all commits

View File

@@ -3,6 +3,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import AppRoutes from './AppRoutes'; // move the routing and loading logic here
import { SettingsProvider, useSettingsContext } from './context/SettingsContext';
import { ThemeProvider } from './context/ThemeContext';
import { BookingsListProvider } from './context/BookingContext';
import { NamePrompt } from './components/ui/NamePrompt';
function AppContent() {
@@ -34,7 +35,9 @@ function App() {
return (
<ThemeProvider>
<SettingsProvider>
<AppContent />
<BookingsListProvider>
<AppContent />
</BookingsListProvider>
</SettingsProvider>
</ThemeProvider>
);

View File

@@ -16,128 +16,31 @@ import CoursePage from './pages/CoursePage';
import Profile from './pages/Profile';
import RoomSchedules from './pages/RoomSchedules';
import { useSettingsContext } from './context/SettingsContext';
import { useBookingsListContext } from './context/BookingContext';
const AppRoutes = () => {
const location = useLocation();
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 [showLeaveBanner, setShowLeaveBanner] = useState(false);
const [lastLeftBooking, setLastLeftBooking] = useState(null);
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null);
const { getCurrentUser } = useSettingsContext();
const currentUser = getCurrentUser();
// Mock bookings data
// In a real app, this would come from an API or global state
const [bookings, setBookings] = useState([
{
id: 1,
date: new CalendarDate(2025, 9, 3),
startTime: 4,
endTime: 6,
room: 'G5:7',
roomCategory: 'green',
title: 'Team standup',
participants: [
currentUser,
{ 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',
roomCategory: 'red',
title: 'Project planning workshop',
participants: [
currentUser,
{ 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',
roomCategory: 'blue',
title: '1:1 with supervisor',
participants: [
currentUser,
{ 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',
roomCategory: 'yellow',
title: 'Study group session',
participants: [
currentUser,
{ id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' },
{ id: 12, name: 'Oskar Pettersson', username: 'ospe3698', email: 'oskar.pettersson@dsv.su.se' }
]
},
{
id: 5,
date: new CalendarDate(2025, 9, 7),
startTime: 10,
endTime: 12,
room: 'G5:9',
roomCategory: 'blue',
title: 'Design review meeting',
createdBy: { id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' },
participants: [
{ id: 3, name: 'Hedvig Engelmark', username: 'heen9876', email: 'hedvig.engelmark@dsv.su.se' },
currentUser,
{ id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' },
{ id: 8, name: 'Erik Larsson', username: 'erla7892', email: 'erik.larsson@dsv.su.se' }
],
isParticipantBooking: true
}
]);
function addBooking(newBooking) {
setBookings([...bookings, newBooking]);
setLastCreatedBooking(newBooking);
setShowSuccessBanner(true);
}
function updateBooking(updatedBooking) {
setBookings(bookings.map(booking =>
booking.id === updatedBooking.id ? updatedBooking : booking
));
setLastUpdatedBooking(updatedBooking);
setShowUpdateBanner(true);
}
function deleteBooking(bookingToDelete, actionType = 'delete') {
setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id));
if (actionType === 'leave') {
setLastLeftBooking(bookingToDelete);
setShowLeaveBanner(true);
} else {
setLastDeletedBooking(bookingToDelete);
setShowDeleteBanner(true);
}
}
// Get bookings data and functions from context
const {
bookings,
addBooking,
updateBooking,
deleteBooking,
showSuccessBanner,
setShowSuccessBanner,
lastCreatedBooking,
showDeleteBanner,
setShowDeleteBanner,
lastDeletedBooking,
showLeaveBanner,
setShowLeaveBanner,
lastLeftBooking,
showUpdateBanner,
setShowUpdateBanner,
lastUpdatedBooking
} = useBookingsListContext();
useEffect(() => {
// Reset scroll position on route change
@@ -161,6 +64,7 @@ const AppRoutes = () => {
<Route path="/" element={<Layout />}>
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} showLeaveBanner={showLeaveBanner} lastLeftBooking={lastLeftBooking} onDismissLeaveBanner={() => setShowLeaveBanner(false)} showUpdateBanner={showUpdateBanner} lastUpdatedBooking={lastUpdatedBooking} onDismissUpdateBanner={() => setShowUpdateBanner(false)} />} />
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
<Route path="new-booking/:roomType" element={<NewBooking addBooking={addBooking} />} />
<Route path="booking-details" element={<BookingDetails addBooking={addBooking} />} />
<Route path="booking-confirmation" element={<BookingConfirmation addBooking={addBooking} />} />
<Route path="course-schedule" element={<CourseSchedule />} />

View File

@@ -4,17 +4,36 @@ import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import styles from './RoomSelectionField.module.css';
export function RoomSelectionField({ clean = false }) {
export function RoomSelectionField({ clean = false, roomTypeFilter = null }) {
const booking = useBookingContext();
const { settings } = useSettingsContext();
// Generate room options based on settings
// Generate room options based on settings and optional filter
const roomOptions = useMemo(() => {
return Array.from({ length: settings.numberOfRooms }, (_, i) => ({
value: `G5:${i + 1}`,
label: `G5:${i + 1}`,
}));
}, [settings.numberOfRooms]);
let rooms = [];
if (!roomTypeFilter || roomTypeFilter === 'litet-grupprum') {
// Add small rooms G5:1-15
rooms = rooms.concat(
Array.from({ length: settings.numberOfRooms }, (_, i) => ({
value: `G5:${i + 1}`,
label: `G5:${i + 1}`,
}))
);
}
if (!roomTypeFilter || roomTypeFilter === 'stort-grupprum') {
// Add large rooms G10:1-7
rooms = rooms.concat(
Array.from({ length: 7 }, (_, i) => ({
value: `G10:${i + 1}`,
label: `G10:${i + 1}`,
}))
);
}
return rooms;
}, [settings.numberOfRooms, roomTypeFilter]);
return (
<div>

View File

@@ -70,7 +70,7 @@ const Navigation = () => {
<div className={styles.top}>
<div className={styles.left}>
<Link to="/" className={styles.logo}>
<img src="su-logo-white.svg" alt="Logo" />
<img src="/su-logo-white.svg" alt="Logo" />
</Link>
<span className={styles.brandText}>Studentportalen</span>
</div>

View File

@@ -295,4 +295,47 @@ export const DEFAULT_DISABLED_OPTIONS = {
6: false,
7: false,
8: true,
};
};
export const RANDOM_BOOKING_NAMES = [
"Projektmöte grupp 7",
"Tentapluggang",
"Metodarbete",
"Grupparbete IS1",
"Redovisning projekt",
"Handledning",
"Kravanalys meeting",
"Pluggsession statistik",
"Gruppstudier DIFO",
"Projektplanering",
"Kodgranskning",
"Retrospektiv möte",
"Scrum planning",
"Design workshop",
"Testning och debug",
"Databasdesign",
"Rapport skrivning",
"Presentationsövning",
"Peer review",
"Brainstorm session",
"Algoritm genomgång",
"UX research möte",
"Prototyping",
"Stakeholder meeting",
"Code review session",
"Agile standup",
"Requirements workshop",
"System arkitektur",
"User testing",
"Demo förberedelse",
"Krisgrupp - allt är sönder",
"Panikprogrammering",
"Buggjakt extreme edition",
"Kaffepaus (viktigt möte)",
"Stack Overflow support group",
"Deadline depression circle",
"Merge conflict therapy",
"Git blame shame session",
"Procrastination workshop",
"Är det fredag än? mötet"
];

View File

@@ -1,7 +1,12 @@
import React, { createContext, useContext } from 'react';
import React, { createContext, useContext, useState } from 'react';
import { CalendarDate } from '@internationalized/date';
import { useSettingsContext } from './SettingsContext';
import { RANDOM_BOOKING_NAMES, PEOPLE } from '../constants/bookingConstants';
const BookingContext = createContext(null);
const BookingsListContext = createContext(null);
// Provider for individual booking forms (existing functionality)
export function BookingProvider({ children, value }) {
return (
<BookingContext.Provider value={value}>
@@ -10,10 +15,194 @@ export function BookingProvider({ children, value }) {
);
}
// Provider for managing the list of all bookings
export function BookingsListProvider({ children }) {
const { getCurrentUser, getEffectiveToday } = useSettingsContext();
const currentUser = getCurrentUser();
const today = getEffectiveToday();
// Helper functions for random generation
const getRandomElement = (array) => array[Math.floor(Math.random() * array.length)];
const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const getRoomCategory = (roomName) => {
const roomNumber = parseInt(roomName.split(':')[1]);
if (roomName.startsWith('G5:')) {
// Small rooms G5:1-15
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';
} else if (roomName.startsWith('G10:')) {
// Large rooms G10:1-7
return 'purple';
}
return 'green';
};
const generateRandomRoom = (isLargeRoom) => {
if (isLargeRoom) {
const roomNum = getRandomInt(1, 7);
return `G10:${roomNum}`;
} else {
const roomNum = getRandomInt(1, 15);
return `G5:${roomNum}`;
}
};
const generateRandomParticipants = (currentUser, isLargeRoom, isCurrentUserBooking) => {
const maxParticipants = isLargeRoom ? 10 : 5;
const minParticipants = 2; // At least creator + 1 participant
const numParticipants = getRandomInt(minParticipants, maxParticipants);
// Get random people from PEOPLE array
const shuffledPeople = [...PEOPLE].sort(() => Math.random() - 0.5);
const selectedPeople = shuffledPeople.slice(0, numParticipants - 1); // -1 because we'll add current user or creator
if (isCurrentUserBooking) {
// Current user is the creator, add them first
return [currentUser, ...selectedPeople];
} else {
// Someone else is the creator, current user may or may not be a participant
const creator = selectedPeople[0];
const otherParticipants = selectedPeople.slice(1);
// 50% chance current user is a participant in other people's bookings
if (Math.random() < 0.5) {
return [creator, currentUser, ...otherParticipants];
} else {
return [creator, ...otherParticipants];
}
}
};
// Generate bookings relative to current date
const generateInitialBookings = () => {
const numBookings = getRandomInt(2, 10);
const bookings = [];
for (let i = 0; i < numBookings; i++) {
const isLargeRoom = Math.random() < 0.2; // 20% chance of large room
const isCurrentUserBooking = Math.random() > 0.2; // 80% current user's bookings, 20% others'
const room = generateRandomRoom(isLargeRoom);
const participants = generateRandomParticipants(currentUser, isLargeRoom, isCurrentUserBooking);
const creator = participants[0];
// Random date within next 14 days
const daysFromNow = getRandomInt(0, 13);
const date = today.add({ days: daysFromNow });
// Random time slots (8:00-17:30, in 30-minute slots)
const startTimeSlot = getRandomInt(0, 19); // 8:00-17:30
const duration = getRandomInt(1, Math.min(4, 19 - startTimeSlot)); // 30min to 2h, don't exceed 17:30
const endTimeSlot = startTimeSlot + duration;
const booking = {
id: i + 1,
date: date,
startTime: startTimeSlot,
endTime: endTimeSlot,
room: room,
roomCategory: getRoomCategory(room),
title: getRandomElement(RANDOM_BOOKING_NAMES),
participants: participants,
createdBy: creator,
isParticipantBooking: !isCurrentUserBooking && participants.includes(currentUser)
};
bookings.push(booking);
}
// Sort by date and time
return bookings.sort((a, b) => {
if (a.date.compare(b.date) !== 0) {
return a.date.compare(b.date);
}
return a.startTime - b.startTime;
});
};
// Initial bookings data (relative to current date)
const [bookings, setBookings] = useState(() => generateInitialBookings());
// Banner states (moved from AppRoutes)
const [showSuccessBanner, setShowSuccessBanner] = useState(false);
const [lastCreatedBooking, setLastCreatedBooking] = useState(null);
const [showDeleteBanner, setShowDeleteBanner] = useState(false);
const [lastDeletedBooking, setLastDeletedBooking] = useState(null);
const [showLeaveBanner, setShowLeaveBanner] = useState(false);
const [lastLeftBooking, setLastLeftBooking] = useState(null);
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
const [lastUpdatedBooking, setLastUpdatedBooking] = useState(null);
// Booking management functions (moved from AppRoutes)
function addBooking(newBooking) {
setBookings([...bookings, newBooking]);
setLastCreatedBooking(newBooking);
setShowSuccessBanner(true);
}
function updateBooking(updatedBooking) {
setBookings(bookings.map(booking =>
booking.id === updatedBooking.id ? updatedBooking : booking
));
setLastUpdatedBooking(updatedBooking);
setShowUpdateBanner(true);
}
function deleteBooking(bookingToDelete, actionType = 'delete') {
setBookings(bookings.filter(booking => booking.id !== bookingToDelete.id));
if (actionType === 'leave') {
setLastLeftBooking(bookingToDelete);
setShowLeaveBanner(true);
} else {
setLastDeletedBooking(bookingToDelete);
setShowDeleteBanner(true);
}
}
const value = {
bookings,
addBooking,
updateBooking,
deleteBooking,
showSuccessBanner,
setShowSuccessBanner,
lastCreatedBooking,
showDeleteBanner,
setShowDeleteBanner,
lastDeletedBooking,
showLeaveBanner,
setShowLeaveBanner,
lastLeftBooking,
showUpdateBanner,
setShowUpdateBanner,
lastUpdatedBooking
};
return (
<BookingsListContext.Provider value={value}>
{children}
</BookingsListContext.Provider>
);
}
// Hook for individual booking forms (existing functionality)
export function useBookingContext() {
const context = useContext(BookingContext);
if (!context) {
throw new Error('useBookingContext must be used within a BookingProvider');
}
return context;
}
// Hook for accessing the list of all bookings
export function useBookingsListContext() {
const context = useContext(BookingsListContext);
if (!context) {
throw new Error('useBookingsListContext must be used within a BookingsListProvider');
}
return context;
}

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import styles from './NewBooking.module.css';
import { TimeCardContainer } from '../components/ui/TimeCardContainer';
import { BookingDatePicker } from '../components/forms/BookingDatePicker';
@@ -12,6 +13,7 @@ import PageContainer from '../components/layout/PageContainer';
import Breadcrumbs from '../components/ui/Breadcrumbs';
export function NewBooking({ addBooking }) {
const { roomType } = useParams();
const { getEffectiveToday, settings } = useSettingsContext();
const booking = useBookingState(addBooking, getEffectiveToday());
const [showFilters, setShowFilters] = useState(false);
@@ -23,6 +25,31 @@ export function NewBooking({ addBooking }) {
// Check if we should use inline form (hide title and participants from main form)
const useInlineForm = settings.bookingFormType === 'inline';
// Get page title, subtitle, and image based on room type
const getPageInfo = () => {
if (roomType === 'litet-grupprum') {
return {
title: 'Litet grupprum',
subtitle: 'Plats för upp till 5 personer',
imageUrl: '/grupprum.jpg'
};
} else if (roomType === 'stort-grupprum') {
return {
title: 'Stort grupprum',
subtitle: 'Plats för upp till 10 personer',
imageUrl: '/stort-grupprum.jpg'
};
} else {
return {
title: 'Boka grupprum',
subtitle: 'Välj från tillgängliga rum',
imageUrl: '/grupprum.jpg'
};
}
};
const { title: pageTitle, subtitle: pageSubtitle, imageUrl } = getPageInfo();
// Check if any filters are active
const hasActiveFilters = booking.selectedRoom !== "allRooms" || booking.selectedBookingLength > 0;
@@ -72,9 +99,9 @@ export function NewBooking({ addBooking }) {
<>
<div className={styles.headerSection}>
<PageHeader
title="Litet grupprum"
subtitle="Plats för 5 personer"
imageUrl="./grupprum.jpg"
title={pageTitle}
subtitle={pageSubtitle}
imageUrl={imageUrl}
breadcrumbs={<Breadcrumbs items={breadcrumbItems} />}
/>
</div>
@@ -92,7 +119,7 @@ export function NewBooking({ addBooking }) {
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<RoomSelectionField roomTypeFilter={roomType} />
<BookingLengthField />
</div>
</div>
@@ -113,7 +140,7 @@ export function NewBooking({ addBooking }) {
{showFilters && (
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<RoomSelectionField roomTypeFilter={roomType} />
<BookingLengthField />
</div>
{hasActiveFilters && (
@@ -149,9 +176,9 @@ export function NewBooking({ addBooking }) {
/* Stacked layout (original) */
<>
<PageHeader
title="Litet grupprum"
subtitle="Plats för 5 personer"
imageUrl="./grupprum.jpg"
title={pageTitle}
subtitle={pageSubtitle}
imageUrl={imageUrl}
breadcrumbs={<Breadcrumbs items={breadcrumbItems} />}
/>
<div className={styles.formContainer}>
@@ -167,7 +194,7 @@ export function NewBooking({ addBooking }) {
/* Always-visible filters */
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<RoomSelectionField roomTypeFilter={roomType} />
<BookingLengthField />
</div>
</div>
@@ -188,7 +215,7 @@ export function NewBooking({ addBooking }) {
{showFilters && (
<div className={styles.filtersContent}>
<div className={styles.filtersRow}>
<RoomSelectionField />
<RoomSelectionField roomTypeFilter={roomType} />
<BookingLengthField />
</div>
{hasActiveFilters && (

View File

@@ -32,10 +32,10 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
<h2 className={styles.sectionHeading}>Ny bokning</h2>
<div className={styles.roomCategoryCards}>
<Link to='/new-booking'>
<Link to='/new-booking/litet-grupprum'>
<Card imageUrl="./grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />
</Link>
<Link to='/new-booking'>
<Link to='/new-booking/stort-grupprum'>
<Card imageUrl="./stort-grupprum.jpg" header="Stort grupprum" subheader="Plats för 10 personer" />
</Link>
</div>