improving-week-36 #1

Merged
jare2473 merged 41 commits from improving-week-36 into main 2025-09-04 10:49:05 +02:00
10 changed files with 357 additions and 56 deletions
Showing only changes of commit d103930e78 - Show all commits

View File

@@ -11,6 +11,7 @@ import FullScreenLoader from './components/FullScreenLoader';
const AppRoutes = () => {
const location = useLocation();
const [loading, setLoading] = useState(false);
const [showSuccessBanner, setShowSuccessBanner] = useState(false);
const [bookings, setBookings] = useState([
{
id: 1,
@@ -18,6 +19,7 @@ const AppRoutes = () => {
startTime: 4,
endTime: 6,
room: 'G5:7',
roomCategory: 'green',
title: 'Team standup',
participants: [
{ id: 2, name: 'Filip Norgren', username: 'fino2341', email: 'filip.norgren@dsv.su.se' },
@@ -31,6 +33,7 @@ const AppRoutes = () => {
startTime: 8,
endTime: 12,
room: 'G5:12',
roomCategory: 'red',
title: 'Project planning workshop',
participants: [
{ id: 5, name: 'Victor Magnusson', username: 'vima8734', email: 'victor.magnusson@dsv.su.se' },
@@ -47,6 +50,7 @@ const AppRoutes = () => {
startTime: 2,
endTime: 3,
room: 'G5:3',
roomCategory: 'blue',
title: '1:1 with supervisor',
participants: [
{ id: 251, name: 'Arjohn Emilsson', username: 'arem1532', email: 'arjohn.emilsson@dsv.su.se' }
@@ -58,6 +62,7 @@ const AppRoutes = () => {
startTime: 6,
endTime: 8,
room: 'G5:15',
roomCategory: 'yellow',
title: 'Study group session',
participants: [
{ id: 11, name: 'Emma Johansson', username: 'emjo4512', email: 'emma.johansson@dsv.su.se' },
@@ -68,6 +73,7 @@ const AppRoutes = () => {
function addBooking(newBooking) {
setBookings([...bookings, newBooking]);
setShowSuccessBanner(true);
}
useEffect(() => {
@@ -84,7 +90,7 @@ const AppRoutes = () => {
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<RoomBooking bookings={bookings} />} />
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} onDismissBanner={() => setShowSuccessBanner(false)} />} />
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
<Route path="booking-settings" element={<BookingSettings />} />
</Route>

View File

@@ -12,6 +12,10 @@ function BookingCard({ booking, onClick }) {
return `${hours}:${minutes === 0 ? '00' : '30'}`;
}
function getRoomCategoryClass(category) {
return `room-${category}`;
}
function formatParticipants(participants) {
@@ -30,21 +34,20 @@ function BookingCard({ booking, onClick }) {
return (
<div className={styles.card} onClick={onClick}>
<div className={styles.header}>
<div className={styles.dateContainer}>
<div className={styles.leftSection}>
<span className={styles.date}>{convertDateObjectToString(booking.date)}</span>
</div>
<div>
<div className={styles.time}>
{getTimeFromIndex(booking.startTime)} - {getTimeFromIndex(booking.endTime)}
<div className={styles.titleRow}>
<h3 className={styles.title}>{booking.title}</h3>
<span className={`${styles.room} ${styles[getRoomCategoryClass(booking.roomCategory)]}`}>{booking.room}</span>
</div>
<span className={styles.room}>{booking.room}</span>
{booking.participants && booking.participants.length > 0 && (
<p className={styles.participants}>{formatParticipants(booking.participants)}</p>
)}
</div>
<div className={styles.timeSection}>
<div className={styles.startTime}>{getTimeFromIndex(booking.startTime)}</div>
<div className={styles.endTime}>{getTimeFromIndex(booking.endTime)}</div>
</div>
</div>
<div className={styles.body}>
<h3 className={styles.title}>{booking.title}</h3>
{booking.participants && booking.participants.length > 0 && (
<p className={styles.participants}>{formatParticipants(booking.participants)}</p>
)}
</div>
</div>
);

View File

@@ -1,76 +1,110 @@
.card {
border: 1px solid #E5E5E5;
padding: 1rem;
padding: 1.25rem;
width: 100%;
border-radius: 0.75rem;
border-radius: 0.5rem;
background: #fff;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card:hover {
cursor: pointer;
border-color: #007AFF;
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.12);
transform: translateY(-2px);
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
height: 100%;
}
.dateContainer {
display: flex;
flex-direction: column;
gap: 0.25rem;
.leftSection {
flex: 1;
}
.date {
text-transform: uppercase;
font-size: 0.75rem;
font-size: 0.8rem;
font-weight: 600;
color: #666;
letter-spacing: 0.5px;
color: #999;
letter-spacing: 1px;
margin-bottom: 0.5rem;
display: block;
}
.titleRow {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.title {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: #333;
line-height: 1.3;
}
.room {
font-weight: 600;
font-size: 0.875rem;
color: #666;
padding: 0.375rem 0.75rem;
border-radius: 1rem;
white-space: nowrap;
}
.time {
font-size: 1.125rem;
font-weight: 600;
color: #333;
.room-green {
background: #D4EDDA;
color: #155724;
}
.body {
.room-red {
background: #F8D7DA;
color: #721C24;
}
.room-blue {
background: #D1ECF1;
color: #0C5460;
}
.room-yellow {
background: #FFF3CD;
color: #856404;
}
.timeSection {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
align-items: center;
/*background-color:rgba(0, 122, 255, 0.12);*/
min-height: 80px;
gap: 0.2rem;
}
.title {
margin: 0;
font-size: 1rem;
font-weight: 600;
.startTime {
font-size: 1.6rem;
font-weight: 400;
color: #333;
line-height: 1.3;
line-height: 1;
}
.endTime {
font-size: 1.6rem;
font-weight: 400;
color: #acacac;
line-height: 1;
}
.participants {
margin: 0;
font-size: 0.875rem;
color: #666;
display: flex;
align-items: center;
}
.participants::before {
content: "👥";
margin-right: 0.5rem;
font-size: 0.9rem;
color: #999;
}

View File

@@ -1,18 +1,62 @@
import React from 'react';
import React, { useState } from 'react';
import styles from './BookingsList.module.css';
import BookingCard from './BookingCard';
function BookingsList({ bookings, handleEditBooking }) {
function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismissBanner, showMockDataBanner }) {
const [showAll, setShowAll] = useState(false);
const INITIAL_DISPLAY_COUNT = 3;
const displayedBookings = showAll ? bookings : bookings.slice(0, INITIAL_DISPLAY_COUNT);
const hasMoreBookings = bookings.length > INITIAL_DISPLAY_COUNT;
return (
<div className={styles.bookingsListContainer}>
{showSuccessBanner && (
<div className={styles.successBanner}>
<div className={styles.bannerContent}>
<span className={styles.successIcon}></span>
<span>Bokningen har skapats!</span>
</div>
<button
className={styles.bannerCloseButton}
onClick={onDismissBanner}
>
×
</button>
</div>
)}
{showMockDataBanner && (
<div className={styles.mockDataBanner}>
<div className={styles.bannerContent}>
<span className={styles.mockIcon}>🔧</span>
<span>Visar testdata för utveckling</span>
</div>
</div>
)}
<div className={styles.bookingsContainer}>
{bookings.length > 0 ? (
<>
{bookings.map((booking, index) => (
{displayedBookings.map((booking, index) => (
<BookingCard key={index} booking={booking} onClick={() => handleEditBooking(booking)} />
))}
{hasMoreBookings && (
<button
className={styles.showMoreButton}
onClick={() => setShowAll(!showAll)}
>
{showAll ? (
<>
<span>Visa färre</span>
<span></span>
</>
) : (
<>
<span>Visa {bookings.length - INITIAL_DISPLAY_COUNT} fler</span>
<span></span>
</>
)}
</button>
)}
</>
) : (
<p className={styles.message}>Du har inga bokningar just nu</p>

View File

@@ -22,4 +22,117 @@
.heading {
margin: 0;
margin-bottom: 0.5rem;
}
.showMoreButton {
background: #ffffff;
border: none;
border-radius: 0.75rem;
padding: 1rem;
font-size: 1rem;
font-weight: 600;
color: rgb(0, 0, 0);
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
width: 100%;
max-width: 100%;
box-shadow: 0 2px 8px rgba(143, 143, 143, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
box-sizing: border-box;
}
.showMoreButton:hover {
background: #f2f6ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(192, 192, 192, 0.3);
}
.showMoreButton:active {
transform: translateY(0);
}
.successBanner {
background: #F8FFF9;
border: 1px solid #28A745;
border-radius: 0.75rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.15);
}
.bannerContent {
display: flex;
align-items: center;
gap: 1rem;
}
.successIcon {
color: #28A745;
font-size: 1.25rem;
font-weight: bold;
}
.bannerContent span:last-child {
color: #155724;
font-weight: 600;
font-size: 1rem;
}
.bannerCloseButton {
background: none;
border: none;
color: #6C757D;
font-size: 1.5rem;
font-weight: 300;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
line-height: 1;
}
.bannerCloseButton:hover {
background: rgba(108, 117, 125, 0.1);
color: #495057;
}
.mockDataBanner {
background: #FFF8E1;
border: 1px solid #FFB74D;
border-radius: 0.75rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(255, 183, 77, 0.15);
}
.mockIcon {
color: #F57F17;
font-size: 1.25rem;
margin-right: 1rem;
}
.mockDataBanner .bannerContent span:last-child {
color: #E65100;
font-weight: 600;
font-size: 1rem;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -28,6 +28,7 @@ export const SettingsProvider = ({ children }) => {
earliestTimeSlot: 0,
latestTimeSlot: 23,
currentUserName: USER.name,
showMockDataBanner: false,
// Then override with saved values
...parsed,
// Convert date strings back to DateValue objects
@@ -55,6 +56,8 @@ export const SettingsProvider = ({ children }) => {
latestTimeSlot: 23, // 19:30 (last slot ending at 20:00)
// Current user settings
currentUserName: USER.name,
// Mock data banner toggle
showMockDataBanner: false,
};
});

View File

@@ -6,10 +6,24 @@ import {
generateId,
findObjectById
} from '../utils/bookingUtils';
import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants';
import { DEFAULT_BOOKING_TITLE, PEOPLE, USER } from '../constants/bookingConstants';
import { useDisabledOptions } from './useDisabledOptions';
import { useSettingsContext } from '../context/SettingsContext';
function getRoomCategory(roomName) {
// Extract room number from room name (e.g., "G5:7" -> 7)
const roomNumber = parseInt(roomName.split(':')[1]);
// Assign categories based on room number ranges
if (roomNumber >= 1 && roomNumber <= 4) return 'green';
if (roomNumber >= 5 && roomNumber <= 8) return 'red';
if (roomNumber >= 9 && roomNumber <= 12) return 'blue';
if (roomNumber >= 13 && roomNumber <= 15) return 'yellow';
// Default fallback
return 'green';
}
export function useBookingState(addBooking, initialDate = null) {
const { settings } = useSettingsContext();
@@ -100,14 +114,20 @@ export function useBookingState(addBooking, initialDate = null) {
title
});
// Include the current user as a participant if not already added
const allParticipants = participants.find(p => p.id === USER.id)
? participants
: [USER, ...participants];
addBooking({
id: generateId(),
date: selectedDate,
startTime: selectedStartIndex,
endTime: selectedEndIndex,
room: selectedRoom,
roomCategory: getRoomCategory(selectedRoom),
title: title !== "" ? title : DEFAULT_BOOKING_TITLE,
participants: participants
participants: allParticipants
});
resetSelections();

View File

@@ -63,6 +63,31 @@ export function BookingSettings() {
</div>
</div>
<div className={styles.section}>
<h2>Display Settings</h2>
<div className={styles.setting}>
<label htmlFor="mockDataBanner">
<strong>Show Mock Data Banner</strong>
<span className={styles.description}>
Display a banner in the normal location to show mock/demo data
</span>
</label>
<div className={styles.toggleGroup}>
<input
id="mockDataBanner"
type="checkbox"
checked={settings.showMockDataBanner}
onChange={(e) => updateSettings({ showMockDataBanner: e.target.checked })}
className={styles.toggle}
/>
<span className={styles.toggleStatus}>
{settings.showMockDataBanner ? 'Enabled' : 'Disabled'}
</span>
</div>
</div>
</div>
<div className={styles.section}>
<h2>Date Settings</h2>

View File

@@ -186,6 +186,51 @@
text-align: center;
}
.toggleGroup {
display: flex;
align-items: center;
gap: 0.75rem;
}
.toggle {
width: 50px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
border: none;
position: relative;
cursor: pointer;
transition: background-color 0.3s ease;
-webkit-appearance: none;
appearance: none;
}
.toggle:checked {
background: #10b981;
}
.toggle:before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle:checked:before {
transform: translateX(26px);
}
.toggleStatus {
font-weight: 500;
color: #374151;
}
.actions {
display: flex;
flex-direction: column;

View File

@@ -3,19 +3,27 @@ import styles from './RoomBooking.module.css';
import { Link } from 'react-router-dom';
import BookingsList from '../components/BookingsList';
import Card from '../components/Card';
import { useSettingsContext } from '../context/SettingsContext';
export function RoomBooking({ bookings }) {
export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) {
const { settings } = useSettingsContext();
function handleEditBooking(booking) {
console.log(booking);
setIsEditBooking(booking);
// setIsEditBooking(booking); // This line seems to have an error, commenting out
}
return (
<div className={styles.pageContainer}>
<h1 className={styles.pageHeading}>Lokalbokning</h1>
<h2>Mina bokingar</h2>
<BookingsList bookings={bookings} handleEditBooking={handleEditBooking} />
<BookingsList
bookings={bookings}
handleEditBooking={handleEditBooking}
showSuccessBanner={showSuccessBanner}
onDismissBanner={onDismissBanner}
showMockDataBanner={settings.showMockDataBanner}
/>
<h2>Ny bokning</h2>
<Link to='/new-booking'>
<Card imageUrl="/grupprum.jpg" header="Litet grupprum" subheader="Plats för 5 personer" />