Monthly Availability Manager - Copy this React, Tailwind Component to your project
import-React,-{-useEffect,-useState-}-from-"react";-import-{-FiEdit2,-FiTrash2,-FiPlus,-FiX,-FiSearch-}-from-"react-icons/fi";-import-MonthlyModal-from-"./UI/MonthlyModal";-import-{-useSelector,-useDispatch-}-from-"react-redux";-import-{-fetchAllMonths,-deleteMonth,-reset,-}-from-"../../store/monthlyAvailabilitySlice";-const-MonthlyAvailability-=-()-=>-{-const-[isModalOpen,-setIsModalOpen]-=-useState(false);-const-[editingMonth,-setEditingMonth]-=-useState(false);-const-[searchDate,-setSearchDate]-=-useState("");-const-[currentPage,-setCurrentPage]-=-useState(1);-const-dispatch-=-useDispatch();-const-[editing,-setEditing]-=-useState(false);-const-{-months,-isLoading,-isError,-errorMessage-}-=-useSelector(-(state)-=>-state.monthlyAvailability-);-const-itemsPerPage-=-4;-useEffect(()-=>-{-dispatch(fetchAllMonths());-},-[dispatch]);-const-totalPages-=-Math.ceil(months.length-/-itemsPerPage);-const-indexOfLastItem-=-currentPage-*-itemsPerPage;-const-indexOfFirstItem-=-indexOfLastItem---itemsPerPage;-const-currentMonths-=-months.slice(indexOfFirstItem,-indexOfLastItem);-const-handleOpenEditModal-=-(month-=-null)-=>-{-dispatch(reset());-if-(month)-{-setEditing(true);-setEditingMonth(month);-}-setIsModalOpen(true);-};-const-handleOpenAddModal-=-()-=>-{-dispatch(reset());-setEditing(false);-setEditingMonth(null);-setIsModalOpen(true);-};-const-handleCloseModal-=-()-=>-{-dispatch(reset());-setIsModalOpen(false);-setEditingMonth(false);-};-const-handleDelete-=-(id)-=>-{-dispatch(deleteMonth(id));-dispatch(reset());-dispatch(fetchAllMonths());-};-const-handlePageChange-=-(pageNumber)-=>-{-setCurrentPage(pageNumber);-};-//-Helper-function-to-get-month-name-const-getMonthName-=-(monthNumber)-=>-{-const-monthNames-=-[-"January",-"February",-"March",-"April",-"May",-"June",-"July",-"August",-"September",-"October",-"November",-"December",-];-return-monthNames[monthNumber];-//--1-because-array-index-starts-from-0-};-return-(-<div-className="min-h-screen-bg-gray-50-p-6">-<div-className="max-w-6xl-mx-auto">-<div-className="flex-justify-between-items-center-mb-8">-<h1-className="text-3xl-font-bold-text-gray-800">Week-Manager</h1>-<div-className="flex-items-center-space-x-4">-<div-className="relative">-<input-type="date"-value={searchDate}-onChange={(e)-=>-setSearchDate(e.target.value)}-className="pl-10-pr-4-py-2-border-border-gray-300-rounded-lg-focus:outline-none-focus:ring-2-focus:ring-blue-500"-placeholder="Search-by-start-date"-/>-<FiSearch-className="absolute-left-3-top-1/2-transform--translate-y-1/2-text-gray-400"-/>-</div>-<button-onClick={()-=>-handleOpenAddModal()}-className="flex-items-center-px-4-py-2-bg-blue-600-text-white-rounded-lg-hover:bg-blue-700-transition-colors"-aria-label="Add-new-week"->-<FiPlus-className="mr-2"-/>-Add-New-Week-</button>-</div>-</div>-<div-className="bg-white-rounded-xl-shadow-lg-overflow-hidden">-<div-className="overflow-x-auto">-<table-className="min-w-full-divide-y-divide-gray-200">-<thead>-<tr-className="bg-gray-50">-<th-className="px-6-py-3-text-left-text-xs-font-medium-text-gray-500-uppercase-tracking-wider">-Year-</th>-<th-className="px-6-py-3-text-left-text-xs-font-medium-text-gray-500-uppercase-tracking-wider">-Month-</th>-<th-className="px-6-py-3-text-left-text-xs-font-medium-text-gray-500-uppercase-tracking-wider">-Actions-</th>-</tr>-</thead>-<tbody-className="bg-white-divide-y-divide-gray-200">-{isLoading-&&-(-<tr>-<td-colSpan="4"-className="px-6-py-4-whitespace-nowrap-text-sm-text-gray-500"->-Loading...-</td>-</tr>-)}-{isError-&&-(-<tr>-<td-colSpan="4"-className="px-6-py-4-whitespace-nowrap-text-sm-text-red-500"->-{errorMessage}-</td>-</tr>-)}-{currentMonths.map((month)-=>-(-<tr-key={month.id}-className="border-t-border-gray-100">-<td-className="px-6-py-4-whitespace-nowrap-text-sm-text-gray-500">-{month.year}-</td>-<td-className="px-6-py-4-whitespace-nowrap-text-sm-text-gray-500">-{getMonthName(month.month)}-</td>-<td-className="px-6-py-4-whitespace-nowrap-text-sm-font-medium">-<button-onClick={()-=>-handleOpenEditModal(month)}-className="text-blue-600-hover:text-blue-900-mr-4"-aria-label="Edit-week"->-<FiEdit2-className="w-5-h-5"-/>-</button>-<button-onClick={()-=>-handleDelete(month.id)}-className="text-red-600-hover:text-red-900"-aria-label="Delete-week"->-<FiTrash2-className="w-5-h-5"-/>-</button>-</td>-</tr>-))}-</tbody>-</table>-</div>-{/*-Pagination-*/}-<div-className="flex-justify-center-items-center-space-x-2-p-4-border-t-border-gray-100">-{Array.from({-length:-totalPages-},-(_,-i)-=>-i-+-1).map(-(pageNumber)-=>-(-<button-key={pageNumber}-onClick={()-=>-handlePageChange(pageNumber)}-className={`px-4-py-2-rounded-lg-${-currentPage-===-pageNumber-?-"bg-blue-600-text-white"-:-"bg-gray-100-text-gray-600-hover:bg-gray-200"-}`}->-{pageNumber}-</button>-)-)}-</div>-</div>-{isModalOpen-&&-(-<div-className="fixed-inset-0-bg-black-bg-opacity-50-flex-items-center-justify-center-z-50-p-4">-<div-className="bg-white-rounded-xl-w-[80%]-h-[80vh]-overflow-auto-relative">-<MonthlyModal-editingMonth={editingMonth}-handleCloseModal={handleCloseModal}-editing={editing}-/>-<button-onClick={handleCloseModal}-className="absolute-top-4-right-4-text-gray-500-hover:text-gray-700"-aria-label="Close-modal"->-<FiX-className="w-6-h-6"-/>-</button>-</div>-</div>-)}-</div>-</div>-);-};-export-default-MonthlyAvailability;-import-React,-{-useState,-useEffect-}-from-"react";-import-{-FiChevronLeft,-FiChevronRight,-FiClock,-FiCalendar,-FiX,-FiPlus,-}-from-"react-icons/fi";-import-{-useSelector,-useDispatch-}-from-"react-redux";-import-{-fetchAllMonths,-createMonth,-updateMonth,-reset,-}-from-"../../../store/monthlyAvailabilitySlice";-const-MonthlyModal-=-({-editingMonth,-handleCloseModal,-editing-})-=>-{-const-[selectedDate,-setSelectedDate]-=-useState(new-Date());-const-[selectedDay,-setSelectedDay]-=-useState(null);-const-[showModal,-setShowModal]-=-useState(false);-const-[timeSlots,-setTimeSlots]-=-useState({});-const-[newTimeSlot,-setNewTimeSlot]-=-useState("");-const-[customTimeSlots,-setCustomTimeSlots]-=-useState([]);-const-dispatch-=-useDispatch();-const-{-isSuccess,-isError,-errorMessage,-isLoading-}-=-useSelector(-(state)-=>-state.monthlyAvailability-);-const-timeSlotOptions-=-[-"09:00",-"10:00",-"11:00",-"12:00",-"13:00",-"14:00",-"15:00",-"16:00",-"17:00",-];-useEffect(()-=>-{-if-(editing-&&-editingMonth)-{-const-{-year,-month,-days-}-=-editingMonth;-//-Initialize-an-empty-object-for-formatted-timeslots-const-formattedTimeSlots-=-{};-//-Iterate-over-the-days-array-days.forEach((day)-=>-{-const-{-date,-timeslots-}-=-day;-//-Format-date-as-"YYYY-MM-DD"-const-formattedDate-=-`${year}-${String(month-+-1).padStart(-2,-"0"-)}-${String(date).padStart(2,-"0")}`;-//-Initialize-the-date-entry-in-formattedTimeSlots-formattedTimeSlots[formattedDate]-=-{};-//-Iterate-over-the-timeslots-for-the-current-day-timeslots.forEach(({-time,-isAvailable-})-=>-{-formattedTimeSlots[formattedDate][time]-=-isAvailable;-});-});-const-newDate-=-new-Date(editingMonth.year,-editingMonth.month);-setSelectedDate(newDate);-//-Set-the-transformed-data-into-state-setTimeSlots(formattedTimeSlots);-}-},-[editing,-editingMonth]);-useEffect(()-=>-{-if-(isSuccess)-{-dispatch(fetchAllMonths());-handleCloseModal();-}-if-(isError)-{-alert(errorMessage);-dispatch(reset());-}-},-[isSuccess,-isError,-errorMessage]);-const-getDaysInMonth-=-(date)-=>-{-const-year-=-date.getFullYear();-const-month-=-date.getMonth();-const-firstDay-=-new-Date(year,-month,-1);-const-lastDay-=-new-Date(year,-month-+-1,-0);-const-days-=-[];-//-Add-days-from-previous-month-to-start-the-calendar-from-Sunday-const-startPadding-=-firstDay.getDay();-for-(let-i-=-startPadding---1;-i->=-0;-i--)-{-const-day-=-new-Date(year,-month,--i);-days.push(day);-}-//-Add-days-of-current-month-for-(let-i-=-1;-i-<=-lastDay.getDate();-i++)-{-days.push(new-Date(year,-month,-i));-}-//-Add-days-from-next-month-to-complete-the-calendar-const-endPadding-=-42---days.length;-//-6-rows-*-7-days-=-42-for-(let-i-=-1;-i-<=-endPadding;-i++)-{-days.push(new-Date(year,-month-+-1,-i));-}-return-days;-};-const-monthDays-=-getDaysInMonth(selectedDate);-const-handlePrevMonth-=-()-=>-{-const-newDate-=-new-Date(selectedDate);-newDate.setMonth(newDate.getMonth()---1);-setSelectedDate(newDate);-};-const-handleNextMonth-=-()-=>-{-const-newDate-=-new-Date(selectedDate);-newDate.setMonth(newDate.getMonth()-+-1);-setSelectedDate(newDate);-};-const-handleDayClick-=-(day)-=>-{-setSelectedDay(day);-setShowModal(true);-};-const-handleTimeSlotToggle-=-(day,-time)-=>-{-const-dateKey-=-day.toISOString().split("T")[0];-setTimeSlots((prev)-=>-({-...prev,-[dateKey]:-{-...prev[dateKey],-[time]:-!prev[dateKey]?.[time],-},-}));-};-const-handleAddCustomTimeSlot-=-()-=>-{-if-(newTimeSlot-&&-!customTimeSlots.includes(newTimeSlot))-{-setCustomTimeSlots([...customTimeSlots,-newTimeSlot]);-setNewTimeSlot("");-}-};-const-preparePayload-=-(timeSlots)-=>-{-const-daysData-=-Object.keys(timeSlots).map((dateKey)-=>-{-const-timeSlotsData-=-Object.keys(timeSlots[dateKey]).map((time)-=>-({-time:-time,-isAvailable:-timeSlots[dateKey][time],-//-Booked-or-not-}));-return-{-date:-parseInt(dateKey.split("-")[2]),-//-Extract-day-from-date-(1-31)-timeslots:-timeSlotsData,-};-});-return-{-year:-selectedDate.getFullYear(),-//-Current-Year-month:-selectedDate.getMonth(),-//-Current-Month-(0-indexed)-days:-daysData,-//-Array-of-days-with-timeslots-};-};-const-handleUpdate-=-async-()-=>-{-if-(!selectedDay)-{-alert("Please-select-a-valid-day.");-return;-}-const-payload-=-preparePayload(timeSlots);-dispatch(updateMonth({-id:-editingMonth.id,-payload-}));-};-const-handleSubmit-=-async-()-=>-{-if-(!selectedDay)-{-alert("Please-select-a-valid-day.");-return;-}-const-payload-=-preparePayload(timeSlots);-dispatch(createMonth(payload));-};-const-isPastDate-=-(date)-=>-{-const-today-=-new-Date();-today.setHours(0,-0,-0,-0);-return-date-<-today;-};-const-isCurrentMonth-=-(date)-=>-{-return-date.getMonth()-===-selectedDate.getMonth();-};-return-(-<div-className="max-w-7xl-mx-auto-p-4-bg-white-rounded-xl-shadow-lg">-<div-className="mb-6-flex-items-center-justify-between-pr-14-pl-6">-<h2-className="text-2xl-font-bold-text-gray-800-flex-items-center-gap-2">-<FiCalendar-className="text-indigo-600"-/>-Monthly-Calendar-</h2>-<div-className="flex-items-center-gap-4">-{!editing-&&-(-<button-onClick={handlePrevMonth}-className="p-2-rounded-full-hover:bg-gray-100-transition-colors"-aria-label="Previous-month"->-<FiChevronLeft-className="w-6-h-6"-/>-</button>-)}-<span-className="font-medium-text-gray-600">-{selectedDate.toLocaleDateString("en-US",-{-month:-"long",-year:-"numeric",-})}-</span>-{!editing-&&-(-<button-onClick={handleNextMonth}-className="p-2-rounded-full-hover:bg-gray-100-transition-colors"-aria-label="Next-month"->-<FiChevronRight-className="w-6-h-6"-/>-</button>-)}-</div>-</div>-<div-className="grid-grid-cols-7-gap-1-mb-4">-{["Sun",-"Mon",-"Tue",-"Wed",-"Thu",-"Fri",-"Sat"].map((day)-=>-(-<div-key={day}-className="text-center-py-2-font-semibold-text-gray-600"->-{day}-</div>-))}-</div>-<div-className="grid-grid-cols-7-gap-1">-{monthDays.map((day)-=>-(-<div-key={day.toISOString()}-className={`p-2-${-isCurrentMonth(day)-?-isPastDate(day)-?-"bg-gray-100"-:-"bg-indigo-50"-:-"bg-gray-50"-}-${-!isCurrentMonth(day)-?-"opacity-50"-:-""-}-transition-all-hover:shadow-md-rounded-lg`}->-<div-className="text-center-mb-2">-<p-className="text-sm-text-gray-600">{day.getDate()}</p>-</div>-<button-onClick={()-=>-handleDayClick(day)}-disabled={isPastDate(day)-||-!isCurrentMonth(day)}-className={`w-full-py-1-px-2-rounded-md-text-xs-font-medium-transition-colors-${-isPastDate(day)-||-!isCurrentMonth(day)-?-"bg-gray-200-text-gray-500-cursor-not-allowed"-:-"bg-indigo-600-text-white-hover:bg-indigo-700"-}`}-aria-label={`View-time-slots-for-${day.toLocaleDateString()}`}->-Slots-</button>-</div>-))}-</div>-{showModal-&&-selectedDay-&&-(-<div-className="fixed-inset-0-bg-black-bg-opacity-50-flex-items-center-justify-center-z-50">-<div-className="bg-white-rounded-xl-p-6-max-w-md-w-full-max-h-[90vh]-overflow-y-auto">-<div-className="flex-justify-between-items-center-mb-4">-<h3-className="text-xl-font-semibold-text-gray-800">-{selectedDay.toLocaleDateString("en-US",-{-weekday:-"long",-month:-"long",-day:-"numeric",-})}-</h3>-<button-onClick={()-=>-setShowModal(false)}-className="p-2-hover:bg-gray-100-rounded-full-transition-colors"-aria-label="Close-modal"->-<FiX-className="w-5-h-5"-/>-</button>-</div>-<div-className="mb-4">-<div-className="flex-gap-2-mb-2">-<input-type="time"-value={newTimeSlot}-onChange={(e)-=>-setNewTimeSlot(e.target.value)}-className="flex-1-p-2-border-rounded-lg"-/>-<button-onClick={handleAddCustomTimeSlot}-className="p-2-bg-indigo-600-text-white-rounded-lg-hover:bg-indigo-700-transition-colors"->-<FiPlus-/>-</button>-</div>-</div>-<div-className="grid-grid-cols-1-gap-2">-{[...timeSlotOptions,-...customTimeSlots].sort().map((time)-=>-{-const-dateKey-=-selectedDay.toISOString().split("T")[0];-const-isAvailable-=-timeSlots[dateKey]?.[time];-return-(-<button-key={time}-onClick={()-=>-handleTimeSlotToggle(selectedDay,-time)}-className={`p-3-rounded-lg-flex-items-center-justify-between-transition-all-${-isAvailable-?-"bg-green-100-text-green-800"-:-"bg-gray-100-text-gray-800"-}-hover:opacity-80`}-aria-label={`${time}-${isAvailable-?-"Available"-:-""}`}->-<span-className="flex-items-center-gap-2">-<FiClock-/>-{time}-</span>-<span-className="text-sm-font-medium">-{isAvailable-?-"Available"-:-""}-</span>-</button>-);-})}-</div>-</div>-</div>-)}-<div-className="mt-6-flex-justify-end">-<button-onClick={editing-?-handleUpdate-:-handleSubmit}-className={`px-6-py-3-rounded-lg-font-medium-text-white-transition-all-${"bg-indigo-600-hover:bg-indigo-700"}`}->-{editing-?-"Update-"-:-"Create-"}Schedule-</button>-</div>-</div>-);-};-export-default-MonthlyModal;
