improving-week-36 #1

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

View File

@@ -1,75 +0,0 @@
import React, { useState } from 'react';
import styles from './BookingConfirmationBanner.module.css';
import { convertDateObjectToString } from '../helpers';
// Test change again
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

@@ -1,131 +0,0 @@
.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;
}

View File

@@ -1,73 +0,0 @@
import React, { useState } from 'react';
import styles from './BookingDeleteBanner.module.css';
import { convertDateObjectToString } from '../helpers';
function BookingDeleteBanner({ booking, onClose, 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.deleteBanner}>
<div className={styles.bannerContent}>
<span className={styles.deleteIcon}>🗑</span>
<div className={styles.deleteText}>
<div className={styles.titleRow}>
<span className={styles.deleteTitle}>Bokning raderad: {booking.title && <span className={styles.bookingTitle}>{booking.title}</span>}</span>
{isTestBanner && <span className={styles.testLabel}>TEST</span>}
</div>
<span className={styles.deleteDetails}>{formatBookingDetails(booking)}</span>
</div>
</div>
{!showFakeCloseButton && (
<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 BookingDeleteBanner;

View File

@@ -2,8 +2,7 @@ import React, { useState } from 'react';
import { CalendarDate } from '@internationalized/date';
import styles from './BookingsList.module.css';
import BookingCard from './BookingCard';
import BookingConfirmationBanner from './BookingConfirmationBanner';
import BookingDeleteBanner from './BookingDeleteBanner';
import NotificationBanner from './NotificationBanner';
function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingDelete, showSuccessBanner, lastCreatedBooking, onDismissBanner, showDeleteBanner, lastDeletedBooking, onDismissDeleteBanner, showDevelopmentBanner, showBookingConfirmationBanner, showBookingDeleteBanner }) {
const [showAll, setShowAll] = useState(false);
@@ -20,28 +19,29 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
return (
<div className={styles.bookingsListContainer}>
{showSuccessBanner && (
<BookingConfirmationBanner
<NotificationBanner
variant="success"
booking={lastCreatedBooking}
onClose={onDismissBanner}
showCloseButton={true}
/>
)}
{showDeleteBanner && (
<BookingDeleteBanner
<NotificationBanner
variant="delete"
booking={lastDeletedBooking}
onClose={onDismissDeleteBanner}
showCloseButton={true}
/>
)}
{showDevelopmentBanner && (
<div className={styles.developmentBanner}>
<div className={styles.bannerContent}>
<span className={styles.developmentIcon}>🔧</span>
<span>Visar testdata för utveckling</span>
</div>
</div>
<NotificationBanner
variant="development"
/>
)}
{showBookingConfirmationBanner && (
<BookingConfirmationBanner
<NotificationBanner
variant="success"
booking={{
title: 'Projektmöte',
room: 'G5:7',
@@ -54,7 +54,8 @@ function BookingsList({ bookings, handleEditBooking, onBookingUpdate, onBookingD
/>
)}
{showBookingDeleteBanner && (
<BookingDeleteBanner
<NotificationBanner
variant="delete"
booking={{
title: 'Uppföljningsmöte',
room: 'G5:12',

View File

@@ -54,35 +54,6 @@
transform: translateY(0);
}
.bannerContent {
display: flex;
align-items: center;
gap: 1rem;
}
.developmentBanner {
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);
}
.developmentIcon {
color: #F57F17;
font-size: 1.25rem;
margin-right: 1rem;
}
.developmentBanner .bannerContent span:last-child {
color: #E65100;
font-weight: 600;
font-size: 1rem;
}
@keyframes slideDown {

View File

@@ -0,0 +1,129 @@
import React, { useState } from 'react';
import styles from './NotificationBanner.module.css';
import { convertDateObjectToString } from '../helpers';
const BANNER_VARIANTS = {
success: {
icon: '✓',
title: 'Bokning bekräftad:',
className: 'success'
},
delete: {
icon: '🗑️',
title: 'Bokning raderad:',
className: 'delete'
},
development: {
icon: '🔧',
title: 'Visar testdata för utveckling',
className: 'development'
}
};
function NotificationBanner({
variant = 'success',
booking = null,
onClose,
showCloseButton = false,
showFakeCloseButton = false,
isTestBanner = false,
customTitle = null,
customContent = null
}) {
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}`;
}
const handleFakeClose = () => {
setShowTooltip(true);
setTimeout(() => setShowTooltip(false), 3000);
};
const config = BANNER_VARIANTS[variant] || BANNER_VARIANTS.success;
// For development banner, use custom content
if (variant === 'development') {
return (
<div className={`${styles.banner} ${styles[config.className]}`}>
<div className={styles.bannerContent}>
<span className={styles.developmentIcon}>{config.icon}</span>
<span>{customTitle || config.title}</span>
</div>
</div>
);
}
// For booking-related banners (success/delete)
if (!booking && !customContent) return null;
return (
<div className={`${styles.banner} ${styles[config.className]}`}>
<div className={styles.bannerContent}>
<span className={`${styles.icon} ${styles[config.className + 'Icon']}`}>
{config.icon}
</span>
<div className={styles.text}>
<div className={styles.titleRow}>
<span className={`${styles.title} ${styles[config.className + 'Title']}`}>
{customTitle || config.title} {booking && booking.title && <span className={styles.bookingTitle}>{booking.title}</span>}
</span>
{isTestBanner && <span className={styles.testLabel}>TEST</span>}
</div>
{booking && (
<span className={`${styles.details} ${styles[config.className + 'Details']}`}>
{formatBookingDetails(booking)}
</span>
)}
{customContent && (
<span className={`${styles.details} ${styles[config.className + 'Details']}`}>
{customContent}
</span>
)}
</div>
</div>
{showCloseButton && (
<button
className={styles.closeButton}
onClick={onClose}
>
×
</button>
)}
{showFakeCloseButton && (
<div className={styles.fakeCloseContainer}>
<button
className={styles.closeButton}
onClick={handleFakeClose}
>
×
</button>
{showTooltip && (
<div className={styles.tooltip}>
Detta är en testbanner som inte kan stängas
</div>
)}
</div>
)}
</div>
);
}
export default NotificationBanner;

View File

@@ -1,13 +1,12 @@
.deleteBanner {
background: #FFF4F4;
border: 1px solid #F87171;
/* Base banner styles */
.banner {
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(248, 113, 113, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.bannerContent {
@@ -16,9 +15,7 @@
gap: 1rem;
}
.deleteIcon {
background: #EF4444;
color: white;
.icon {
width: 2rem;
height: 2rem;
border-radius: 50%;
@@ -30,7 +27,7 @@
flex-shrink: 0;
}
.deleteText {
.text {
display: flex;
flex-direction: column;
gap: 0.25rem;
@@ -42,19 +39,21 @@
gap: 0.5rem;
}
.deleteTitle {
color: #DC2626;
.title {
font-weight: 700;
font-size: 1.1rem;
}
.deleteDetails {
color: #EF4444;
.details {
font-weight: 500;
font-size: 0.9rem;
}
.bannerCloseButton {
.bookingTitle {
font-weight: 400;
}
.closeButton {
background: none;
border: none;
color: #6C757D;
@@ -69,15 +68,61 @@
width: fit-content;
}
.bannerCloseButton:hover {
.closeButton:hover {
background: rgba(108, 117, 125, 0.1);
color: #495057;
}
.bookingTitle {
font-weight: 400;
/* Success variant styles */
.success {
background: #E8F5E8;
border: 1px solid #4CAF50;
}
.successIcon {
background: #4CAF50;
color: white;
}
.successTitle {
color: #2E7D32;
}
.successDetails {
color: #388E3C;
}
/* Delete variant styles */
.delete {
background: #FFF4F4;
border: 1px solid #F87171;
}
.deleteIcon {
background: #EF4444;
color: white;
}
.deleteTitle {
color: #DC2626;
}
.deleteDetails {
color: #EF4444;
}
/* Development variant styles */
.development {
background: #FFF8E1;
border: 1px solid #FFB74D;
}
.developmentIcon {
font-size: 1.5rem;
color: #FF9800;
}
/* Test label styles */
.testLabel {
background: #FF9800;
color: white;
@@ -89,6 +134,7 @@
letter-spacing: 0.5px;
}
/* Fake close and tooltip styles */
.fakeCloseContainer {
position: relative;
}