From 59ac8d09c02ad9014753733af2a63cc56d32938b Mon Sep 17 00:00:00 2001 From: Anvita Mahajan <78889572+Anvita0305@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:08:49 +0530 Subject: [PATCH] Added Yearly Calendar Screens for admin and user portals (#1911) * Yearly Calender Screens * updated user screen * user-calender-view * locales files updated * fixed linting errors * added tests and changed font --- public/locales/en.json | 4 +- public/locales/fr.json | 4 +- public/locales/hi.json | 4 +- public/locales/sp.json | 4 +- public/locales/zh.json | 4 +- .../EventCalendar/EventCalendar.test.tsx | 41 ++ .../EventCalendar/EventCalendar.tsx | 84 ++-- src/components/EventCalendar/EventHeader.tsx | 6 + .../YearlyEventCalender.module.css | 354 +++++++++++++++++ .../EventCalendar/YearlyEventCalender.tsx | 360 ++++++++++++++++++ .../OrganizationEvents/OrganizationEvents.tsx | 1 + 11 files changed, 829 insertions(+), 37 deletions(-) create mode 100644 src/components/EventCalendar/YearlyEventCalender.module.css create mode 100644 src/components/EventCalendar/YearlyEventCalender.tsx diff --git a/public/locales/en.json b/public/locales/en.json index dc03b2cfdf..0ac40eb06d 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -949,7 +949,9 @@ "startDate": "Start Date", "endDate": "End Date", "publicEvent": "Is Public", - "registerable": "Is Registerable" + "registerable": "Is Registerable", + "monthlyCalendarView": "Monthly Calendar", + "yearlyCalendarView": "Yearly Calender" }, "userEventCard": { "location": "Location", diff --git a/public/locales/fr.json b/public/locales/fr.json index fe04314dfc..c3d0d6b2e9 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -946,7 +946,9 @@ "startDate": "Date de début", "endDate": "Date de fin", "publicEvent": "Est public", - "registerable": "Est enregistrable" + "registerable": "Est enregistrable", + "monthlyCalendarView": "Calendrier mensuel", + "yearlyCalendarView": "Calendrier annuel" }, "userEventCard": { "location": "Emplacement", diff --git a/public/locales/hi.json b/public/locales/hi.json index 2eddd662c8..6b20483163 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -951,7 +951,9 @@ "startDate": "प्रारंभ तिथि", "endDate": "अंतिम तिथि", "publicEvent": "सार्वजनिक है", - "registerable": "पंजीकृत करने योग्य है" + "registerable": "पंजीकृत करने योग्य है", + "monthlyCalendarView": "मासिक कैलेंडर", + "yearlyCalendarView": "वार्षिक कैलेंडर" }, "userEventCard": { "location": "जगह", diff --git a/public/locales/sp.json b/public/locales/sp.json index 203857425c..ae532fe023 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -948,7 +948,9 @@ "startDate": "Fecha de inicio", "endDate": "Fecha de finalización", "publicEvent": "Es público", - "registerable": "Es registrable" + "registerable": "Es registrable", + "monthlyCalendarView": "Calendario mensual", + "yearlyCalendarView": "Calendario anual" }, "userEventCard": { "location": "Ubicación", diff --git a/public/locales/zh.json b/public/locales/zh.json index 5c45bb5987..aee29a9b43 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -949,7 +949,9 @@ "startDate": "开始日期", "endDate": "结束日期", "publicEvent": "公开", - "registerable": "可注册" + "registerable": "可注册", + "monthlyCalendarView": "月历", + "yearlyCalendarView": "年度日历" }, "userEventCard": { "location": "地點", diff --git a/src/components/EventCalendar/EventCalendar.test.tsx b/src/components/EventCalendar/EventCalendar.test.tsx index 3b824685af..657f43a781 100644 --- a/src/components/EventCalendar/EventCalendar.test.tsx +++ b/src/components/EventCalendar/EventCalendar.test.tsx @@ -12,6 +12,7 @@ import { import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; import { weekdays } from './constants'; +import { months } from './constants'; import { BrowserRouter as Router } from 'react-router-dom'; const eventData = [ @@ -92,6 +93,14 @@ const MOCKS = [ const link = new StaticMockLink(MOCKS, true); +async function wait(ms = 200): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + describe('Calendar', () => { it('renders weekdays', () => { render(); @@ -154,6 +163,27 @@ describe('Calendar', () => { fireEvent.click(prevButton); } }); + it('Should show prev and next year on clicking < & > buttons when in year view', async () => { + //testing previous month button + render( + + + + + , + ); + await wait(); + const prevButtons = screen.getAllByTestId('prevYear'); + prevButtons.forEach((button) => { + fireEvent.click(button); + }); + await wait(); + //testing next year button + const nextButton = screen.getAllByTestId('prevYear'); + nextButton.forEach((button) => { + fireEvent.click(button); + }); + }); it('Should show prev and next date on clicking < & > buttons in the day view', async () => { render( @@ -370,4 +400,15 @@ describe('Calendar', () => { window.dispatchEvent(new Event('resize')); }); }); + it('renders year view', async () => { + render(); + + await wait(); + months.forEach((month) => { + const elements = screen.getAllByText(month); + elements.forEach((element) => { + expect(element).toBeInTheDocument(); + }); + }); + }); }); diff --git a/src/components/EventCalendar/EventCalendar.tsx b/src/components/EventCalendar/EventCalendar.tsx index 4ee1f6dcc2..c06f3c5fea 100644 --- a/src/components/EventCalendar/EventCalendar.tsx +++ b/src/components/EventCalendar/EventCalendar.tsx @@ -9,6 +9,7 @@ import { ViewType } from 'screens/OrganizationEvents/OrganizationEvents'; import HolidayCard from '../HolidayCards/HolidayCard'; import { holidays, hours, months, weekdays } from './constants'; import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils'; +import YearlyEventCalender from './YearlyEventCalender'; interface InterfaceEventListCardProps { userRole?: string; @@ -563,41 +564,47 @@ const Calendar: React.FC = ({ return (
-
- + {viewType != ViewType.YEAR && ( +
+ -
- {viewType == ViewType.DAY ? `${currentDate}` : ``} {currentYear}{' '} -
{months[currentMonth]}
-
- -
+
+ {viewType == ViewType.DAY ? `${currentDate}` : ``} {currentYear}{' '} +
{months[currentMonth]}
+
+
+ +
-
+ )}
{viewType == ViewType.MONTH ? (
@@ -611,8 +618,21 @@ const Calendar: React.FC = ({
{renderDays()}
) : ( - /*istanbul ignore next*/ -
{renderHours()}
+ // +
+ {viewType == ViewType.YEAR ? ( + + ) : ( +
{renderHours()}
+ )} +
+ )} +
+
+ {viewType == ViewType.YEAR ? ( + + ) : ( +
{renderHours()}
)}
diff --git a/src/components/EventCalendar/EventHeader.tsx b/src/components/EventCalendar/EventHeader.tsx index 0ccc8fd600..a4bc59596c 100644 --- a/src/components/EventCalendar/EventHeader.tsx +++ b/src/components/EventCalendar/EventHeader.tsx @@ -65,6 +65,12 @@ function eventHeader({ {ViewType.DAY} + + {ViewType.YEAR} +
diff --git a/src/components/EventCalendar/YearlyEventCalender.module.css b/src/components/EventCalendar/YearlyEventCalender.module.css new file mode 100644 index 0000000000..723cb20e22 --- /dev/null +++ b/src/components/EventCalendar/YearlyEventCalender.module.css @@ -0,0 +1,354 @@ +.calendar { + font-family: sans-serif; + font-size: 1.2rem; + margin-bottom: 20px; +} +.calendar__header { + display: flex; + margin-bottom: 2rem; + align-items: center; + margin: 0px 10px 0px 10px; +} +.input { + flex: 1; + position: relative; +} +.calendar__header_month { + margin: 0.5rem; + color: #707070; + font-weight: bold; +} +.space { + flex: 1; + display: flex; + align-items: center; + justify-content: space-around; +} +.button { + color: #707070; + background-color: rgba(0, 0, 0, 0); + font-weight: bold; + border: 0px; +} +.calendar__weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + background-color: #707070; + height: 60px; +} +.calendar__scroll { + height: 80vh; + padding: 10px; +} +.weekday { + display: flex; + justify-content: center; + align-items: center; + color: #fff; + background-color: #31bb6b; + font-weight: 600; +} +.calendar__days { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-template-rows: repeat(6, 1fr); +} +.calendar_hour_text_container { + display: flex; + flex-direction: row; + align-items: flex-end; + border-right: 1px solid #8d8d8d55; + width: 40px; +} +.calendar_hour_text { + top: -10px; + left: -5px; + position: relative; + color: #707070; + font-size: 12px; +} +.calendar_timezone_text { + top: -10px; + left: -11px; + position: relative; + color: #707070; + font-size: 9px; +} +.calendar_hour_block { + display: flex; + flex-direction: row; + border-bottom: 1px solid #8d8d8d55; + position: relative; + height: 50px; + border-bottom-right-radius: 5px; +} +.event_list_parent { + position: relative; + width: 100%; +} +.event_list_parent_current { + background-color: #def6e1; + position: relative; + width: 100%; +} +.dummyWidth { + width: 1px; +} +.day { + background-color: #ffffff; + padding-left: 0.3rem; + padding-right: 0.3rem; + background-color: white; + border: 1px solid #8d8d8d55; + color: black; + font-weight: 600; + height: 8rem; + position: relative; +} +.day__outside { + /* background-color: #ededee !important; */ + color: #89898996 !important; +} +.day__selected { + background-color: #007bff; + color: #707070; +} +.day__today { + background-color: #def6e1; + font-weight: 700; + text-decoration: underline; + color: #31bb6b; +} +.day__events { + background-color: #def6e1; +} +/* yearly calender styling */ +.yearlyCalender { + background-color: #ffffff; + box-sizing: border-box; +} + +.circularButton { + width: 25px; + height: 25px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0); + border: none; + cursor: pointer; + margin-left: 0.75rem; +} +.circularButton:hover { + background-color: rgba(82, 172, 255, 0.5); +} +.closebtn { + padding: 10px; +} + +.yearlyCalendarHeader { + display: flex; + flex-direction: row; +} + +.yearlyCalendarHeader > div { + font-weight: 600; + font-size: 2rem; + padding: 0 10px; + color: #4b4b4b; +} +.noEventAvailable { + background-color: rgba(255, 255, 255, 0); +} + +.card { + /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */ + padding: 16px; + text-align: center; + /* background-color: #f1f1f1; */ + height: 21rem; +} +.cardHeader { + text-align: left; +} + +.row { + margin: 1px -5px; +} + +/* Clear floats after the columns */ +.row:after { + content: ''; + display: table; + clear: both; +} + +.weekday__yearly { + display: flex; + justify-content: center; + align-items: center; + /* color: #fff; */ + background-color: #ffffff; + font-weight: 600; +} +.day__yearly { + background-color: #ffffff; + padding-left: 0.3rem; + padding-right: 0.3rem; + background-color: white; + /* border: 1px solid #8d8d8d55; */ + color: #4b4b4b; + font-weight: 600; + height: 2rem; + position: relative; +} +* { + box-sizing: border-box; +} + +/* Float four columns side by side */ +.column { + float: left; + width: 25%; + padding: 10px; +} + +/* Remove extra left and right margins, due to padding */ +.row { + margin: 0 -5px; +} + +/* Clear floats after the columns */ +.row:after { + content: ''; + display: table; + clear: both; +} +/* yearly calender styling ends */ +.btn__today { + transition: ease-in all 200ms; + font-family: Arial; + color: #ffffff; + font-size: 18px; + padding: 10px 20px 10px 20px; + text-decoration: none; + margin-left: 20px; + border: none; +} +.btnsContainer { + display: flex; + margin: 2.5rem 0 2.5rem 0; +} + +.btnsContainer .btnsBlock { + display: flex; +} + +.btnsContainer .btnsBlock button { + margin-left: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +.dropdown { + border-color: #31bb6b; + background-color: white; + color: #31bb6b; + box-shadow: 0px 2px 1px rgba(49, 187, 107, 0.5); /* Added blur effect */ +} + +.btnsContainer .input { + flex: 1; + position: relative; +} + +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); +} + +.btnsContainer .input button { + width: 52px; +} +.searchBtn { + margin-bottom: 10px; +} + +.inputField { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px #31bb6b; +} +.inputField > button { + padding-top: 10px; + padding-bottom: 10px; +} +.btn__more { + border: 0px; + font-size: 14px; + background-color: initial; + font-weight: 600; + transition: all 200ms; + position: relative; + display: block; + margin: -9px; + margin-top: -28px; +} +.btn__more:hover { + color: #3ce080; +} + +.expand_event_list { + display: block; +} +.list_container { + padding: 5px; + width: fit-content; + display: flex; + flex-direction: row; +} +.event_list_hour { + display: flex; + flex-direction: row; +} +.expand_list_container { + width: 200px; + max-height: 250px; + z-index: 10; + position: absolute; + left: auto; + right: auto; + overflow: auto; + padding: 10px 4px 0px 4px; + background-color: rgb(241, 241, 241); + border: 1px solid rgb(201, 201, 201); + border-radius: 5px; + margin: 5px; +} +.flex_grow { + flex-grow: 1; +} + +@media only screen and (max-width: 700px) { + .event_list { + display: none; + } + .expand_list_container { + width: 150px; + padding: 4px 4px 0px 4px; + } + .day { + height: 5rem; + } +} + +@media only screen and (max-width: 500px) { + .btn__more { + font-size: 12px; + } + + .column { + float: left; + width: 100%; + padding: 10px; + } +} diff --git a/src/components/EventCalendar/YearlyEventCalender.tsx b/src/components/EventCalendar/YearlyEventCalender.tsx new file mode 100644 index 0000000000..2bf8a6eb9d --- /dev/null +++ b/src/components/EventCalendar/YearlyEventCalender.tsx @@ -0,0 +1,360 @@ +import EventListCard from 'components/EventListCard/EventListCard'; +import dayjs from 'dayjs'; +import Button from 'react-bootstrap/Button'; +import React, { useState, useEffect } from 'react'; +import styles from './YearlyEventCalender.module.css'; +import { ViewType } from 'screens/OrganizationEvents/OrganizationEvents'; +import { ChevronLeft, ChevronRight } from '@mui/icons-material'; +import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils'; + +interface InterfaceEventListCardProps { + userRole?: string; + key?: string; + _id: string; + location: string; + title: string; + description: string; + startDate: string; + endDate: string; + startTime: string | null; + endTime: string | null; + allDay: boolean; + recurring: boolean; + recurrenceRule: InterfaceRecurrenceRule | null; + isRecurringEventException: boolean; + isPublic: boolean; + isRegisterable: boolean; + attendees?: { + _id: string; + }[]; + creator?: { + firstName: string; + lastName: string; + _id: string; + }; +} + +interface InterfaceCalendarProps { + eventData: InterfaceEventListCardProps[]; + refetchEvents?: () => void; + orgData?: InterfaceIOrgList; + userRole?: string; + userId?: string; + viewType?: ViewType; +} + +enum Status { + ACTIVE = 'ACTIVE', + BLOCKED = 'BLOCKED', + DELETED = 'DELETED', +} + +enum Role { + USER = 'USER', + SUPERADMIN = 'SUPERADMIN', + ADMIN = 'ADMIN', +} + +interface InterfaceIEventAttendees { + userId: string; + user?: string; + status?: Status; + createdAt?: Date; +} + +interface InterfaceIOrgList { + admins: { _id: string }[]; +} +const Calendar: React.FC = ({ + eventData, + refetchEvents, + orgData, + userRole, + userId, +}) => { + const [selectedDate] = useState(null); + const weekdaysShorthand = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + const today = new Date(); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [events, setEvents] = useState( + null, + ); + const [expandedY, setExpandedY] = useState(null); + + const filterData = ( + eventData: InterfaceEventListCardProps[], + orgData?: InterfaceIOrgList, + userRole?: string, + userId?: string, + ): InterfaceEventListCardProps[] => { + const data: InterfaceEventListCardProps[] = []; + if (userRole === Role.SUPERADMIN) return eventData; + // Hard to test all the cases + /* istanbul ignore next */ + if (userRole === Role.ADMIN) { + eventData?.forEach((event) => { + if (event.isPublic) data.push(event); + if (!event.isPublic) { + const filteredOrg: boolean | undefined = orgData?.admins?.some( + (data) => data._id === userId, + ); + + if (filteredOrg) { + data.push(event); + } + } + }); + } else { + eventData?.forEach((event) => { + if (event.isPublic) data.push(event); + const userAttending = event.attendees?.some( + (data) => data._id === userId, + ); + if (userAttending) { + data.push(event); + } + }); + } + return data; + }; + + useEffect(() => { + const data = filterData(eventData, orgData, userRole, userId); + setEvents(data); + }, [eventData, orgData, userRole, userId]); + + const handlePrevYear = (): void => { + /*istanbul ignore next*/ + setCurrentYear(currentYear - 1); + }; + + const handleNextYear = (): void => { + /*istanbul ignore next*/ + setCurrentYear(currentYear + 1); + }; + + const renderMonthDays = (): JSX.Element[] => { + const renderedMonths: JSX.Element[] = []; + + for (let monthInx = 0; monthInx < 12; monthInx++) { + const monthStart = new Date(currentYear, monthInx, 1); + const monthEnd = new Date(currentYear, monthInx + 1, 0); + + const startDate = new Date(monthStart); + const dayOfWeek = startDate.getDay(); + const diff = startDate.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); + startDate.setDate(diff); + + const endDate = new Date(monthEnd); + const endDayOfWeek = endDate.getDay(); + const diffEnd = + endDate.getDate() + (7 - endDayOfWeek) - (endDayOfWeek === 0 ? 7 : 0); + endDate.setDate(diffEnd); + + const days = []; + let currentDate = startDate; + while (currentDate <= endDate) { + days.push(currentDate); + currentDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + currentDate.getDate() + 1, + ); + } + + const renderedDays = days.map((date, dayIndex) => { + const className = [ + date.toLocaleDateString() === today.toLocaleDateString() + ? styles.day__today + : '', + date.getMonth() !== monthInx ? styles.day__outside : '', + selectedDate?.getTime() === date.getTime() + ? styles.day__selected + : '', + styles.day__yearly, + ].join(' '); + + const eventsForCurrentDate = events?.filter((event) => { + return dayjs(event.startDate).isSame(date, 'day'); + }); + + /*istanbul ignore next*/ + const renderedEvents = + eventsForCurrentDate?.map((datas: InterfaceEventListCardProps) => { + const attendees: { _id: string }[] = []; + datas.attendees?.forEach((attendee: { _id: string }) => { + const r = { + _id: attendee._id, + }; + + attendees.push(r); + }); + + return ( + + ); + }) || []; + + /*istanbul ignore next*/ + const toggleExpand = (index: string): void => { + if (expandedY === index) { + setExpandedY(null); + } else { + setExpandedY(index); + } + }; + + /*istanbul ignore next*/ + return ( +
+ {date.getDate()} +
+
+ {expandedY === `${monthInx}-${dayIndex}` && renderedEvents} +
+ {renderedEvents && renderedEvents?.length > 0 && ( + + )} + {renderedEvents && renderedEvents?.length == 0 && ( + + )} +
+
+ ); + }); + + renderedMonths.push( +
+
+
{months[monthInx]}
+
+ {weekdaysShorthand.map((weekday, index) => ( +
+ {weekday} +
+ ))} +
+
{renderedDays}
+
+
, + ); + } + + return renderedMonths; + }; + + const renderYearlyCalendar = (): JSX.Element => { + return ( +
+
+ +
{currentYear}
+ +
+ +
+
{renderMonthDays()}
+
+
+ ); + }; + + return ( +
+
+
{renderYearlyCalendar()}
+
+
+ ); +}; + +export default Calendar; diff --git a/src/screens/OrganizationEvents/OrganizationEvents.tsx b/src/screens/OrganizationEvents/OrganizationEvents.tsx index ca4508b9bf..0bf561381f 100644 --- a/src/screens/OrganizationEvents/OrganizationEvents.tsx +++ b/src/screens/OrganizationEvents/OrganizationEvents.tsx @@ -37,6 +37,7 @@ const timeToDayJs = (time: string): Dayjs => { export enum ViewType { DAY = 'Day', MONTH = 'Month View', + YEAR = 'Year View', } function organizationEvents(): JSX.Element {