improving-week-36 #1

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

View File

@@ -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>

View 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;

View 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;
}

View File

@@ -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 ? (
<>

View File

@@ -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;

View File

@@ -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,
};
});

View File

@@ -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>

View File

@@ -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'>