booking-flow-finalized-design kindaaaa #7
@@ -1,16 +1,40 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import AppRoutes from './AppRoutes'; // move the routing and loading logic here
|
||||
import { SettingsProvider } from './context/SettingsContext';
|
||||
import { SettingsProvider, useSettingsContext } from './context/SettingsContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { NamePrompt } from './components/ui/NamePrompt';
|
||||
|
||||
function AppContent() {
|
||||
const { shouldShowNamePrompt } = useSettingsContext();
|
||||
const [showNamePrompt, setShowNamePrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNamePrompt(shouldShowNamePrompt());
|
||||
}, [shouldShowNamePrompt]);
|
||||
|
||||
const handleCloseNamePrompt = () => {
|
||||
setShowNamePrompt(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Router basename={import.meta.env.BASE_URL}>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
<NamePrompt
|
||||
isOpen={showNamePrompt}
|
||||
onClose={handleCloseNamePrompt}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<SettingsProvider>
|
||||
<Router basename={import.meta.env.BASE_URL}>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
<AppContent />
|
||||
</SettingsProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import Home from './pages/Home';
|
||||
import CoursePage from './pages/CoursePage';
|
||||
import Profile from './pages/Profile';
|
||||
import RoomSchedules from './pages/RoomSchedules';
|
||||
import { useSettingsContext } from './context/SettingsContext';
|
||||
|
||||
const AppRoutes = () => {
|
||||
const location = useLocation();
|
||||
@@ -27,6 +28,11 @@ const AppRoutes = () => {
|
||||
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,
|
||||
@@ -37,7 +43,7 @@ const AppRoutes = () => {
|
||||
roomCategory: 'green',
|
||||
title: 'Team standup',
|
||||
participants: [
|
||||
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
|
||||
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' }
|
||||
@@ -52,7 +58,7 @@ const AppRoutes = () => {
|
||||
roomCategory: 'red',
|
||||
title: 'Project planning workshop',
|
||||
participants: [
|
||||
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
|
||||
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' },
|
||||
@@ -70,7 +76,7 @@ const AppRoutes = () => {
|
||||
roomCategory: 'blue',
|
||||
title: '1:1 with supervisor',
|
||||
participants: [
|
||||
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
|
||||
currentUser,
|
||||
{ id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }
|
||||
]
|
||||
},
|
||||
@@ -83,7 +89,7 @@ const AppRoutes = () => {
|
||||
roomCategory: 'yellow',
|
||||
title: 'Study group session',
|
||||
participants: [
|
||||
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@dsv.su.se' },
|
||||
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' }
|
||||
]
|
||||
@@ -99,7 +105,7 @@ const AppRoutes = () => {
|
||||
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' },
|
||||
{ id: 1, name: 'Jacob Reinikainen', username: 'jare2473', email: 'jacob.reinikainen@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' }
|
||||
],
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ParticipantsSelector } from '../forms/ParticipantsSelector';
|
||||
import { useBookingContext } from '../../context/BookingContext';
|
||||
import { useSettingsContext } from '../../context/SettingsContext';
|
||||
import { generateId } from '../../utils/bookingUtils';
|
||||
import { USER } from '../../constants/bookingConstants';
|
||||
import styles from './InlineModalBookingForm.module.css';
|
||||
import extendedStyles from './InlineModalExtendedBookingForm.module.css';
|
||||
|
||||
@@ -41,6 +40,7 @@ export function InlineModalExtendedBookingForm({
|
||||
const navigate = useNavigate();
|
||||
const booking = useBookingContext();
|
||||
const { getCurrentUser, getDefaultBookingTitle } = useSettingsContext();
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
// Initialize with pre-selected end time if available, or auto-select if only 30 min available
|
||||
const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength :
|
||||
@@ -161,9 +161,9 @@ export function InlineModalExtendedBookingForm({
|
||||
const roomToBook = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom;
|
||||
|
||||
// Include the current user as a participant if not already added
|
||||
const allParticipants = booking.participants.find(p => p.id === USER.id)
|
||||
const allParticipants = booking.participants.find(p => p.id === currentUser.id)
|
||||
? booking.participants
|
||||
: [USER, ...booking.participants];
|
||||
: [currentUser, ...booking.participants];
|
||||
|
||||
const finalTitle = booking.title !== "" ? booking.title : getDefaultBookingTitle();
|
||||
|
||||
@@ -204,9 +204,9 @@ export function InlineModalExtendedBookingForm({
|
||||
</p>
|
||||
{(() => {
|
||||
// Include the current user as a participant if not already added
|
||||
const allParticipants = booking.participants.find(p => p.id === USER.id)
|
||||
const allParticipants = booking.participants.find(p => p.id === currentUser.id)
|
||||
? booking.participants
|
||||
: [USER, ...booking.participants];
|
||||
: [currentUser, ...booking.participants];
|
||||
|
||||
const startTime = getTimeFromIndex(booking.selectedStartIndex);
|
||||
const endTime = getTimeFromIndex(booking.selectedEndIndex);
|
||||
|
||||
107
my-app/src/components/ui/NamePrompt.jsx
Normal file
107
my-app/src/components/ui/NamePrompt.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Dialog, Heading, Modal } from 'react-aria-components';
|
||||
import { useSettingsContext } from '../../context/SettingsContext';
|
||||
import styles from './NamePrompt.module.css';
|
||||
|
||||
export function NamePrompt({ isOpen, onClose }) {
|
||||
const { updateSettings } = useSettingsContext();
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Helper function to generate username from name
|
||||
const generateUsername = (fullName) => {
|
||||
const nameParts = fullName.trim().toLowerCase().split(' ');
|
||||
if (nameParts.length >= 2) {
|
||||
const firstName = nameParts[0];
|
||||
const lastName = nameParts[nameParts.length - 1];
|
||||
// Take first 2 chars of first name + first 2 chars of last name + random 4 digits
|
||||
const randomDigits = Math.floor(1000 + Math.random() * 9000);
|
||||
return `${firstName.substring(0, 2)}${lastName.substring(0, 2)}${randomDigits}`;
|
||||
} else {
|
||||
// Fallback for single name
|
||||
const randomDigits = Math.floor(1000 + Math.random() * 9000);
|
||||
return `${nameParts[0].substring(0, 4)}${randomDigits}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to generate email from username
|
||||
const generateEmail = (username) => {
|
||||
return `${username}@student.su.se`;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
setError('Vänligen ange ditt namn');
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedName = name.trim();
|
||||
const generatedUsername = generateUsername(trimmedName);
|
||||
const generatedEmail = generateEmail(generatedUsername);
|
||||
|
||||
// Update settings with name, username, and email
|
||||
updateSettings({
|
||||
currentUserName: trimmedName,
|
||||
currentUserUsername: generatedUsername,
|
||||
currentUserEmail: generatedEmail
|
||||
});
|
||||
localStorage.setItem('hasSeenNamePrompt', 'true');
|
||||
onClose();
|
||||
|
||||
// Refresh the page to ensure all components get the updated user data
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setName(e.target.value);
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
isDismissable={false}
|
||||
className={styles.modal}
|
||||
>
|
||||
<Dialog className={styles.dialog}>
|
||||
<div className={styles.content}>
|
||||
<Heading slot="title" className={styles.title}>
|
||||
Välkommen!
|
||||
</Heading>
|
||||
<p className={styles.description}>
|
||||
För att komma igång behöver vi veta vad du heter. Ditt namn kommer att visas i bokningar och deltagarlistan.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<div className={styles.inputGroup}>
|
||||
<label htmlFor="userName" className={styles.label}>
|
||||
Ditt namn
|
||||
</label>
|
||||
<input
|
||||
id="userName"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={handleInputChange}
|
||||
className={`${styles.input} ${error ? styles.inputError : ''}`}
|
||||
placeholder="Ange ditt för- och efternamn"
|
||||
autoFocus
|
||||
/>
|
||||
{error && <span className={styles.errorMessage}>{error}</span>}
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.submitButton}
|
||||
>
|
||||
Fortsätt
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
132
my-app/src/components/ui/NamePrompt.module.css
Normal file
132
my-app/src/components/ui/NamePrompt.module.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 480px;
|
||||
width: 90vw;
|
||||
padding: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
color: #111827;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #6b7280;
|
||||
margin: 0 0 24px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.inputError:focus {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: 14px;
|
||||
color: #ef4444;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.submitButton:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.submitButton:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.submitButton:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@
|
||||
.container:active,
|
||||
.container[data-pressed] {
|
||||
background-color: var(--timecard-active-bg);
|
||||
transform: translateY(1px);
|
||||
box-shadow: var(--timecard-active-shadow);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
@@ -84,7 +83,11 @@
|
||||
border: 1px solid var(--timecard-unavailable-border);
|
||||
height: 50px;
|
||||
width: 165px;
|
||||
background: red;
|
||||
}
|
||||
|
||||
.unavailableSlot:active {
|
||||
background-color: var(--timecard-unavailable-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
@@ -142,13 +145,11 @@
|
||||
|
||||
.saveButton:active {
|
||||
background-color: var(--modal-save-active-bg);
|
||||
transform: translateY(1px);
|
||||
box-shadow: var(--modal-save-active-shadow);
|
||||
}
|
||||
|
||||
.cancelButton:active {
|
||||
background-color: var(--modal-cancel-active-bg);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.timeSpan {
|
||||
|
||||
@@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = {
|
||||
earliestTimeSlot: 0,
|
||||
latestTimeSlot: 23,
|
||||
currentUserName: USER.name,
|
||||
currentUserUsername: USER.username,
|
||||
currentUserEmail: USER.email,
|
||||
showDevelopmentBanner: false,
|
||||
showBookingConfirmationBanner: false,
|
||||
showBookingDeleteBanner: false,
|
||||
@@ -44,8 +46,10 @@ export const SettingsProvider = ({ children }) => {
|
||||
...parsed,
|
||||
// Convert date strings back to DateValue objects
|
||||
mockToday: parsed.mockToday ? new Date(parsed.mockToday) : null,
|
||||
// Ensure currentUserName has a fallback
|
||||
// Ensure user fields have fallbacks
|
||||
currentUserName: parsed.currentUserName || USER.name,
|
||||
currentUserUsername: parsed.currentUserUsername || USER.username,
|
||||
currentUserEmail: parsed.currentUserEmail || USER.email,
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse saved settings:', e);
|
||||
@@ -55,6 +59,13 @@ export const SettingsProvider = ({ children }) => {
|
||||
return DEFAULT_SETTINGS;
|
||||
});
|
||||
|
||||
// Check if user should see name prompt
|
||||
const shouldShowNamePrompt = () => {
|
||||
const hasSeenPrompt = localStorage.getItem('hasSeenNamePrompt');
|
||||
const hasCustomName = settings.currentUserName !== USER.name;
|
||||
return !hasSeenPrompt && !hasCustomName;
|
||||
};
|
||||
|
||||
// Save settings to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
const toSave = {
|
||||
@@ -93,8 +104,8 @@ export const SettingsProvider = ({ children }) => {
|
||||
return {
|
||||
id: USER.id,
|
||||
name: settings.currentUserName,
|
||||
username: USER.username,
|
||||
email: USER.email
|
||||
username: settings.currentUserUsername,
|
||||
email: settings.currentUserEmail
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,6 +123,7 @@ export const SettingsProvider = ({ children }) => {
|
||||
getEffectiveToday,
|
||||
getCurrentUser,
|
||||
getDefaultBookingTitle,
|
||||
shouldShowNamePrompt,
|
||||
}}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
|
||||
@@ -6,6 +6,8 @@ import styles from './Profile.module.css';
|
||||
const Profile = () => {
|
||||
const { getCurrentUser } = useSettingsContext();
|
||||
const user = getCurrentUser();
|
||||
|
||||
console.log(user, "Current User Data");
|
||||
|
||||
// Helper function to get user's initials
|
||||
const getInitials = (name) => {
|
||||
|
||||
@@ -26,14 +26,7 @@ export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, o
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTestSessionActive && (
|
||||
<div className={styles.welcomeSection}>
|
||||
<div className={styles.welcomeContent}>
|
||||
<h1 className={styles.welcomeTitle}>Välkommen, {settings.currentUserName}!</h1>
|
||||
<p className={styles.welcomeSubtitle}>Hantera dina bokningar och reservera nya lokaler</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PageHeader title="Lokalbokning" subtitle="Reservera lokaler för möten och studier" />
|
||||
<PageContainer>
|
||||
<h2 className={styles.sectionHeading}>Ny bokning</h2>
|
||||
|
||||
Reference in New Issue
Block a user