improving-week-36 #1
118
my-app/src/components/ParticipantsSelector.jsx
Normal file
118
my-app/src/components/ParticipantsSelector.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { PEOPLE } from '../constants/bookingConstants';
|
||||
import { useBookingContext } from '../context/BookingContext';
|
||||
import styles from './ParticipantsSelector.module.css';
|
||||
|
||||
export function ParticipantsSelector() {
|
||||
const booking = useBookingContext();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [recentSearches, setRecentSearches] = useState([]);
|
||||
const inputRef = useRef(null);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
// Filter people based on search term
|
||||
const filteredPeople = PEOPLE.filter(person =>
|
||||
person.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
person.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Show all people when empty search, filtered when typing
|
||||
const displayPeople = searchTerm === '' ? PEOPLE : filteredPeople;
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setIsDropdownOpen(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setIsDropdownOpen(true);
|
||||
};
|
||||
|
||||
const handleSelectPerson = (person) => {
|
||||
console.log('handleSelectPerson called with:', person);
|
||||
booking.handleParticipantChange(person.id);
|
||||
setSearchTerm('');
|
||||
setIsDropdownOpen(false);
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleRemoveParticipant = (participantToRemove) => {
|
||||
booking.handleRemoveParticipant(participantToRemove);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h3 className={styles.elementHeading}>Deltagare</h3>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className={styles.searchContainer} ref={dropdownRef}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleInputFocus}
|
||||
placeholder="Search for participants..."
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
|
||||
{/* Dropdown */}
|
||||
{isDropdownOpen && (
|
||||
<div className={styles.dropdown}>
|
||||
{displayPeople.length > 0 ? (
|
||||
<div className={styles.section}>
|
||||
{displayPeople.map((person) => (
|
||||
<div
|
||||
key={person.id}
|
||||
className={styles.dropdownItem}
|
||||
onClick={() => handleSelectPerson(person)}
|
||||
>
|
||||
<div className={styles.personInfo}>
|
||||
<div className={styles.personName}>{person.name}</div>
|
||||
<div className={styles.personEmail}>{person.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.noResults}>
|
||||
No participants found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Participants */}
|
||||
{booking.participants.length > 0 && (
|
||||
<div className={styles.selectedParticipants}>
|
||||
{booking.participants.map((participant, index) => (
|
||||
<div key={index} className={styles.participantChip}>
|
||||
<span className={styles.participantName}>{participant}</span>
|
||||
<button
|
||||
className={styles.removeButton}
|
||||
onClick={() => handleRemoveParticipant(participant)}
|
||||
type="button"
|
||||
title="Remove participant"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
my-app/src/components/ParticipantsSelector.module.css
Normal file
182
my-app/src/components/ParticipantsSelector.module.css
Normal file
@@ -0,0 +1,182 @@
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.elementHeading {
|
||||
margin: 0;
|
||||
color: #8E8E8E;
|
||||
font-size: 0.8rem;
|
||||
font-style: normal;
|
||||
font-weight: 520;
|
||||
line-height: normal;
|
||||
margin-bottom: 0.2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.selectedParticipants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.participantChip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #F0F8FF;
|
||||
border: 1px solid #D1E7FF;
|
||||
border-radius: 1.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: #2563EB;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.participantChip:hover {
|
||||
background-color: #E0F2FE;
|
||||
border-color: #BAE6FD;
|
||||
}
|
||||
|
||||
.participantName {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
border: none;
|
||||
color: #2563EB;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.removeButton:hover {
|
||||
background-color: rgba(37, 99, 235, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #D2D9E0;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 16px;
|
||||
background-color: #FAFBFC;
|
||||
padding: 1rem;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchInput::placeholder {
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
.searchInput:focus {
|
||||
outline: 2px solid var(--focus-ring-color, #3e70ec);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #D2D9E0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1000;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.section:not(:last-child) {
|
||||
border-bottom: 1px solid #F1F3F4;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
font-weight: 600;
|
||||
color: #5F6368;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 0.25rem 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dropdownItem:hover {
|
||||
background-color: #F8F9FA;
|
||||
}
|
||||
|
||||
.dropdownItem:active {
|
||||
background-color: #E8F0FE;
|
||||
}
|
||||
|
||||
.personInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.personName {
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.personEmail {
|
||||
font-size: 0.75rem;
|
||||
color: #5F6368;
|
||||
}
|
||||
|
||||
.addNewItem {
|
||||
color: #1A73E8;
|
||||
font-weight: 500;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.addNewItem:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #5F6368;
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -19,12 +19,12 @@ export const SMALL_GROUP_ROOMS = Array.from({ length: 15 }, (_, i) => ({
|
||||
}));
|
||||
|
||||
export const PEOPLE = [
|
||||
{ id: 1, name: 'Arjohn Emilsson' },
|
||||
{ id: 2, name: 'Filip Norgren' },
|
||||
{ id: 3, name: 'Hedvig Engelmark' },
|
||||
{ id: 4, name: 'Elin Rudling' },
|
||||
{ id: 5, name: 'Victor Magnusson' },
|
||||
{ id: 6, name: 'Ellen Britschgi' }
|
||||
{ id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' },
|
||||
{ id: 2, name: 'Filip Norgren', email: 'filip.norgren@dsv.su.se' },
|
||||
{ id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' },
|
||||
{ id: 4, name: 'Elin Rudling', email: 'elin.rudling@dsv.su.se' },
|
||||
{ id: 5, name: 'Victor Magnusson', email: 'victor.magnusson@dsv.su.se' },
|
||||
{ id: 6, name: 'Ellen Britschgi', email: 'ellen.britschgi@dsv.su.se' }
|
||||
];
|
||||
|
||||
export const DEFAULT_DISABLED_OPTIONS = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
generateId,
|
||||
findObjectById
|
||||
} from '../utils/bookingUtils';
|
||||
import { DEFAULT_BOOKING_TITLE } from '../constants/bookingConstants';
|
||||
import { DEFAULT_BOOKING_TITLE, PEOPLE } from '../constants/bookingConstants';
|
||||
import { useDisabledOptions } from './useDisabledOptions';
|
||||
|
||||
export function useBookingState(addBooking) {
|
||||
@@ -98,11 +98,24 @@ export function useBookingState(addBooking) {
|
||||
}
|
||||
}, [selectedEndIndex]);
|
||||
|
||||
const handleParticipantChange = useCallback((participant) => {
|
||||
if (participant !== null) {
|
||||
setParticipants(prev => [...prev, participant.trim()]);
|
||||
const handleParticipantChange = useCallback((participantId) => {
|
||||
console.log('handleParticipantChange called with:', participantId);
|
||||
if (participantId !== null && participantId !== undefined) {
|
||||
// Find the person by ID and add their name
|
||||
const person = PEOPLE.find(p => p.id === participantId);
|
||||
console.log('Found person:', person);
|
||||
if (person && !participants.includes(person.name)) {
|
||||
console.log('Adding participant:', person.name);
|
||||
setParticipants(prev => [...prev, person.name]);
|
||||
} else {
|
||||
console.log('Participant already exists or person not found');
|
||||
}
|
||||
setParticipant("");
|
||||
}
|
||||
}, [participants]);
|
||||
|
||||
const handleRemoveParticipant = useCallback((participantToRemove) => {
|
||||
setParticipants(prev => prev.filter(p => p !== participantToRemove));
|
||||
}, []);
|
||||
|
||||
// Memoize the return object to prevent unnecessary re-renders
|
||||
@@ -134,6 +147,7 @@ export function useBookingState(addBooking) {
|
||||
handleSave,
|
||||
handleTimeCardExit,
|
||||
handleParticipantChange,
|
||||
handleRemoveParticipant,
|
||||
}), [
|
||||
timeSlotsByRoom,
|
||||
currentRoom,
|
||||
@@ -155,5 +169,6 @@ export function useBookingState(addBooking) {
|
||||
handleSave,
|
||||
handleTimeCardExit,
|
||||
handleParticipantChange,
|
||||
handleRemoveParticipant,
|
||||
]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import styles from './NewBooking.module.css';
|
||||
import { TimeCardContainer } from '../components/TimeCardContainer';
|
||||
import { BookingDatePicker } from '../components/BookingDatePicker';
|
||||
import { BookingTitleField } from '../components/BookingTitleField';
|
||||
import { ParticipantsField } from '../components/ParticipantsField';
|
||||
import { ParticipantsSelector } from '../components/ParticipantsSelector';
|
||||
import { RoomSelectionField } from '../components/RoomSelectionField';
|
||||
import { BookingLengthField } from '../components/BookingLengthField';
|
||||
import { useBookingState } from '../hooks/useBookingState';
|
||||
@@ -19,7 +19,7 @@ export function NewBooking({ addBooking }) {
|
||||
<div className={styles.formContainer}>
|
||||
<main style={{ flex: 1 }}>
|
||||
<BookingTitleField />
|
||||
<ParticipantsField />
|
||||
<ParticipantsSelector />
|
||||
|
||||
<div className={styles.bookingTimesContainer}>
|
||||
<BookingDatePicker />
|
||||
|
||||
Reference in New Issue
Block a user