improving-week-36 #1
@@ -12,6 +12,7 @@ const AppRoutes = () => {
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSuccessBanner, setShowSuccessBanner] = useState(false);
|
||||
const [lastCreatedBooking, setLastCreatedBooking] = useState(null);
|
||||
const [bookings, setBookings] = useState([
|
||||
{
|
||||
id: 1,
|
||||
@@ -73,6 +74,7 @@ const AppRoutes = () => {
|
||||
|
||||
function addBooking(newBooking) {
|
||||
setBookings([...bookings, newBooking]);
|
||||
setLastCreatedBooking(newBooking);
|
||||
setShowSuccessBanner(true);
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ const AppRoutes = () => {
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} onDismissBanner={() => setShowSuccessBanner(false)} />} />
|
||||
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} />} />
|
||||
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
|
||||
<Route path="booking-settings" element={<BookingSettings />} />
|
||||
</Route>
|
||||
|
||||
73
my-app/src/components/BookingConfirmationBanner.jsx
Normal file
73
my-app/src/components/BookingConfirmationBanner.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import styles from './BookingConfirmationBanner.module.css';
|
||||
import { convertDateObjectToString } from '../helpers';
|
||||
|
||||
function BookingConfirmationBanner({ booking, onClose, showCloseButton = false, showFakeCloseButton = false, isTestBanner = false }) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
function getTimeFromIndex(timeIndex) {
|
||||
const totalHalfHoursFromStart = timeIndex;
|
||||
const totalMinutes = 8 * 60 + totalHalfHoursFromStart * 30; // 8:00 as base
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
return `${hours}:${minutes === 0 ? '00' : '30'}`;
|
||||
}
|
||||
|
||||
function formatBookingDetails(booking) {
|
||||
if (!booking) return '';
|
||||
|
||||
const dateStr = convertDateObjectToString(booking.date);
|
||||
const startTime = getTimeFromIndex(booking.startTime);
|
||||
const endTime = getTimeFromIndex(booking.endTime);
|
||||
|
||||
return `${booking.room} • ${dateStr} • ${startTime}-${endTime}`;
|
||||
}
|
||||
|
||||
if (!booking) return null;
|
||||
|
||||
const handleFakeClose = () => {
|
||||
setShowTooltip(true);
|
||||
setTimeout(() => setShowTooltip(false), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.confirmationBanner}>
|
||||
<div className={styles.bannerContent}>
|
||||
<span className={styles.confirmationIcon}>✓</span>
|
||||
<div className={styles.confirmationText}>
|
||||
<div className={styles.titleRow}>
|
||||
<span className={styles.confirmationTitle}>Bokning bekräftad {booking.title && <span className={styles.bookingTitle}>{booking.title}</span>}</span>
|
||||
{isTestBanner && <span className={styles.testLabel}>TEST</span>}
|
||||
</div>
|
||||
<span className={styles.confirmationDetails}>{formatBookingDetails(booking)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
className={styles.bannerCloseButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
{showFakeCloseButton && (
|
||||
<div className={styles.fakeCloseContainer}>
|
||||
<button
|
||||
className={styles.bannerCloseButton}
|
||||
onClick={handleFakeClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{showTooltip && (
|
||||
<div className={styles.tooltip}>
|
||||
Detta är en testbanner som inte kan stängas
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookingConfirmationBanner;
|
||||
132
my-app/src/components/BookingConfirmationBanner.module.css
Normal file
132
my-app/src/components/BookingConfirmationBanner.module.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.confirmationBanner {
|
||||
background: #E8F5E8;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
|
||||
}
|
||||
|
||||
.bannerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.confirmationIcon {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.confirmationText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmationTitle {
|
||||
color: #2E7D32;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.testLabel {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.confirmationDetails {
|
||||
color: #388E3C;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bannerCloseButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6C757D;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.bannerCloseButton:hover {
|
||||
background: rgba(108, 117, 125, 0.1);
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.fakeCloseContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.tooltip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 1rem;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #333;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bookingTitle {
|
||||
font-weight: 400;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import styles from './BookingsList.module.css';
|
||||
import BookingCard from './BookingCard';
|
||||
import BookingConfirmationBanner from './BookingConfirmationBanner';
|
||||
|
||||
function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismissBanner, showMockDataBanner }) {
|
||||
function BookingsList({ bookings, handleEditBooking, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDevelopmentBanner, showBookingConfirmationBanner }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const INITIAL_DISPLAY_COUNT = 3;
|
||||
|
||||
@@ -12,27 +14,33 @@ function BookingsList({ bookings, handleEditBooking, showSuccessBanner, onDismis
|
||||
return (
|
||||
<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>
|
||||
<BookingConfirmationBanner
|
||||
booking={lastCreatedBooking}
|
||||
onClose={onDismissBanner}
|
||||
showCloseButton={true}
|
||||
/>
|
||||
)}
|
||||
{showMockDataBanner && (
|
||||
<div className={styles.mockDataBanner}>
|
||||
{showDevelopmentBanner && (
|
||||
<div className={styles.developmentBanner}>
|
||||
<div className={styles.bannerContent}>
|
||||
<span className={styles.mockIcon}>🔧</span>
|
||||
<span className={styles.developmentIcon}>🔧</span>
|
||||
<span>Visar testdata för utveckling</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showBookingConfirmationBanner && (
|
||||
<BookingConfirmationBanner
|
||||
booking={{
|
||||
title: 'Projektmöte',
|
||||
room: 'G5:7',
|
||||
date: new CalendarDate(2025, 9, 4),
|
||||
startTime: 4,
|
||||
endTime: 6
|
||||
}}
|
||||
showFakeCloseButton={true}
|
||||
isTestBanner={true}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.bookingsContainer}>
|
||||
{bookings.length > 0 ? (
|
||||
<>
|
||||
|
||||
@@ -55,55 +55,14 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.successBanner {
|
||||
background: #F8FFF9;
|
||||
border: 1px solid #28A745;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.15);
|
||||
}
|
||||
|
||||
.bannerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
color: #28A745;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bannerContent span:last-child {
|
||||
color: #155724;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.bannerCloseButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6C757D;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bannerCloseButton:hover {
|
||||
background: rgba(108, 117, 125, 0.1);
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.mockDataBanner {
|
||||
.developmentBanner {
|
||||
background: #FFF8E1;
|
||||
border: 1px solid #FFB74D;
|
||||
border-radius: 0.75rem;
|
||||
@@ -114,18 +73,19 @@
|
||||
box-shadow: 0 2px 8px rgba(255, 183, 77, 0.15);
|
||||
}
|
||||
|
||||
.mockIcon {
|
||||
.developmentIcon {
|
||||
color: #F57F17;
|
||||
font-size: 1.25rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.mockDataBanner .bannerContent span:last-child {
|
||||
.developmentBanner .bannerContent span:last-child {
|
||||
color: #E65100;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -28,7 +28,8 @@ export const SettingsProvider = ({ children }) => {
|
||||
earliestTimeSlot: 0,
|
||||
latestTimeSlot: 23,
|
||||
currentUserName: USER.name,
|
||||
showMockDataBanner: false,
|
||||
showDevelopmentBanner: false,
|
||||
showBookingConfirmationBanner: false,
|
||||
// Then override with saved values
|
||||
...parsed,
|
||||
// Convert date strings back to DateValue objects
|
||||
@@ -56,8 +57,10 @@ export const SettingsProvider = ({ children }) => {
|
||||
latestTimeSlot: 23, // 19:30 (last slot ending at 20:00)
|
||||
// Current user settings
|
||||
currentUserName: USER.name,
|
||||
// Mock data banner toggle
|
||||
showMockDataBanner: false,
|
||||
// Development banner toggle
|
||||
showDevelopmentBanner: false,
|
||||
// Booking confirmation banner toggle
|
||||
showBookingConfirmationBanner: false,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -67,22 +67,43 @@ export function BookingSettings() {
|
||||
<h2>Display Settings</h2>
|
||||
|
||||
<div className={styles.setting}>
|
||||
<label htmlFor="mockDataBanner">
|
||||
<strong>Show Mock Data Banner</strong>
|
||||
<label htmlFor="developmentBanner">
|
||||
<strong>Show Development Banner</strong>
|
||||
<span className={styles.description}>
|
||||
Display a banner in the normal location to show mock/demo data
|
||||
Display a banner indicating development/test data
|
||||
</span>
|
||||
</label>
|
||||
<div className={styles.toggleGroup}>
|
||||
<input
|
||||
id="mockDataBanner"
|
||||
id="developmentBanner"
|
||||
type="checkbox"
|
||||
checked={settings.showMockDataBanner}
|
||||
onChange={(e) => updateSettings({ showMockDataBanner: e.target.checked })}
|
||||
checked={settings.showDevelopmentBanner}
|
||||
onChange={(e) => updateSettings({ showDevelopmentBanner: e.target.checked })}
|
||||
className={styles.toggle}
|
||||
/>
|
||||
<span className={styles.toggleStatus}>
|
||||
{settings.showMockDataBanner ? 'Enabled' : 'Disabled'}
|
||||
{settings.showDevelopmentBanner ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.setting}>
|
||||
<label htmlFor="bookingConfirmationBanner">
|
||||
<strong>Show Booking Confirmation Banner</strong>
|
||||
<span className={styles.description}>
|
||||
Display a banner that looks like a booking confirmation
|
||||
</span>
|
||||
</label>
|
||||
<div className={styles.toggleGroup}>
|
||||
<input
|
||||
id="bookingConfirmationBanner"
|
||||
type="checkbox"
|
||||
checked={settings.showBookingConfirmationBanner}
|
||||
onChange={(e) => updateSettings({ showBookingConfirmationBanner: e.target.checked })}
|
||||
className={styles.toggle}
|
||||
/>
|
||||
<span className={styles.toggleStatus}>
|
||||
{settings.showBookingConfirmationBanner ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import BookingsList from '../components/BookingsList';
|
||||
import Card from '../components/Card';
|
||||
import { useSettingsContext } from '../context/SettingsContext';
|
||||
|
||||
export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) {
|
||||
export function RoomBooking({ bookings, showSuccessBanner, lastCreatedBooking, onDismissBanner }) {
|
||||
const { settings } = useSettingsContext();
|
||||
|
||||
function handleEditBooking(booking) {
|
||||
@@ -21,8 +21,10 @@ export function RoomBooking({ bookings, showSuccessBanner, onDismissBanner }) {
|
||||
bookings={bookings}
|
||||
handleEditBooking={handleEditBooking}
|
||||
showSuccessBanner={showSuccessBanner}
|
||||
lastCreatedBooking={lastCreatedBooking}
|
||||
onDismissBanner={onDismissBanner}
|
||||
showMockDataBanner={settings.showMockDataBanner}
|
||||
showDevelopmentBanner={settings.showDevelopmentBanner}
|
||||
showBookingConfirmationBanner={settings.showBookingConfirmationBanner}
|
||||
/>
|
||||
<h2>Ny bokning</h2>
|
||||
<Link to='/new-booking'>
|
||||
|
||||
Reference in New Issue
Block a user