- Added loading state to indicate data fetching progress. - Implemented toast notifications for success and error messages during availability operations. - Introduced a delete confirmation modal for improved user experience when removing availability slots. - Enhanced TimeSlotModal with validation error handling and loading indicators for adding time slots.
167 lines
8.2 KiB
TypeScript
167 lines
8.2 KiB
TypeScript
import moment from 'moment'
|
|
import type { CalendarEvent } from '../types'
|
|
|
|
interface TimeSlotModalProps {
|
|
selectedDate: Date
|
|
newTimeSlot: {
|
|
startTime: string
|
|
endTime: string
|
|
}
|
|
existingEvents: CalendarEvent[]
|
|
onTimeSlotChange: (timeSlot: { startTime: string; endTime: string }) => void
|
|
onAdd: () => void
|
|
onCancel: () => void
|
|
isAdding?: boolean
|
|
validationError?: string | null
|
|
}
|
|
|
|
export function TimeSlotModal({
|
|
selectedDate,
|
|
newTimeSlot,
|
|
existingEvents,
|
|
onTimeSlotChange,
|
|
onAdd,
|
|
onCancel,
|
|
isAdding = false,
|
|
validationError = null
|
|
}: TimeSlotModalProps) {
|
|
const getEventsForDate = (date: Date) => {
|
|
return existingEvents.filter(event => {
|
|
const eventDate = new Date(event.start)
|
|
return eventDate.toDateString() === date.toDateString()
|
|
})
|
|
}
|
|
|
|
// Validate time slot
|
|
const isValid = () => {
|
|
if (!newTimeSlot.startTime || !newTimeSlot.endTime) {
|
|
return false
|
|
}
|
|
const [startHour, startMinute] = newTimeSlot.startTime.split(':').map(Number)
|
|
const [endHour, endMinute] = newTimeSlot.endTime.split(':').map(Number)
|
|
const startMinutes = startHour * 60 + startMinute
|
|
const endMinutes = endHour * 60 + endMinute
|
|
return startMinutes < endMinutes
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-999999">
|
|
<div
|
|
className="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[2px]"
|
|
onClick={onCancel}
|
|
/>
|
|
<div className="relative w-full rounded-2xl bg-white shadow-theme-xl dark:bg-gray-900 max-w-md mx-auto p-4" onClick={(e) => e.stopPropagation()}>
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={onCancel}
|
|
className="absolute right-3 top-3 z-999 flex h-9.5 w-9.5 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:right-6 sm:top-6 sm:h-11 sm:w-11"
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
clipRule="evenodd"
|
|
d="M6.04289 16.5413C5.65237 16.9318 5.65237 17.565 6.04289 17.9555C6.43342 18.346 7.06658 18.346 7.45711 17.9555L11.9987 13.4139L16.5408 17.956C16.9313 18.3466 17.5645 18.3466 17.955 17.956C18.3455 17.5655 18.3455 16.9323 17.955 16.5418L13.4129 11.9997L17.955 7.4576C18.3455 7.06707 18.3455 6.43391 17.955 6.04338C17.5645 5.65286 16.9313 5.65286 16.5408 6.04338L11.9987 10.5855L7.45711 6.0439C7.06658 5.65338 6.43342 5.65338 6.04289 6.0439C5.65237 6.43442 5.65237 7.06759 6.04289 7.45811L10.5845 11.9997L6.04289 16.5413Z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Add Availability for {moment(selectedDate).format('MMMM D, YYYY')}
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Start Time
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={newTimeSlot.startTime}
|
|
onChange={(e) => onTimeSlotChange({ ...newTimeSlot, startTime: e.target.value })}
|
|
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
validationError || !isValid() && newTimeSlot.startTime && newTimeSlot.endTime
|
|
? 'border-red-300 dark:border-red-600'
|
|
: 'border-gray-300 dark:border-gray-600'
|
|
}`}
|
|
disabled={isAdding}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
End Time
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={newTimeSlot.endTime}
|
|
onChange={(e) => onTimeSlotChange({ ...newTimeSlot, endTime: e.target.value })}
|
|
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
validationError || !isValid() && newTimeSlot.startTime && newTimeSlot.endTime
|
|
? 'border-red-300 dark:border-red-600'
|
|
: 'border-gray-300 dark:border-gray-600'
|
|
}`}
|
|
disabled={isAdding}
|
|
/>
|
|
{(validationError || (!isValid() && newTimeSlot.startTime && newTimeSlot.endTime)) && (
|
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
|
{validationError || 'End time must be after start time'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Show existing time slots for this date */}
|
|
{getEventsForDate(selectedDate).length > 0 && (
|
|
<div className="mt-4">
|
|
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Existing time slots:
|
|
</h4>
|
|
<div className="space-y-1">
|
|
{getEventsForDate(selectedDate).map(event => (
|
|
<div key={event.id} className="text-sm text-gray-600 dark:text-gray-400">
|
|
{moment(event.start).format('HH:mm')} - {moment(event.end).format('HH:mm')} ({event.title})
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end space-x-3 mt-6">
|
|
<button
|
|
onClick={onCancel}
|
|
disabled={isAdding}
|
|
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={onAdd}
|
|
disabled={!isValid() || isAdding}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
|
|
>
|
|
{isAdding ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Adding...
|
|
</>
|
|
) : (
|
|
'Add Time Slot'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|