From b1592adc66cada71370dd39272504b4ec31226e0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:55:18 +0530 Subject: [PATCH] [WEB-371]: Implemented `react-day-picker` for date selections (#3679) * dev: initialize new date picker * style: selected date focus state * chore: replace custom date filter modal components * chore: replaced inbox snooze popover datepicker * chore: replaced the custom date picker * style: date range picker designed * chore: date range picker implemented throughout the platform * chore: updated tab indices * chore: range-picker in the issue layouts * chore: passed due date color * chore: removed range picker from issue dates --- web/components/api-token/modal/form.tsx | 22 +- .../core/filters/date-filter-modal.tsx | 39 +- web/components/cycles/form.tsx | 76 ++-- web/components/cycles/sidebar.tsx | 363 ++++------------- web/components/dropdowns/date-range.tsx | 261 +++++++++++++ web/components/dropdowns/date.tsx | 40 +- web/components/dropdowns/index.ts | 1 + .../dropdowns/member/project-member.tsx | 2 +- .../dropdowns/member/workspace-member.tsx | 2 +- web/components/inbox/inbox-issue-actions.tsx | 20 +- .../issues/issue-detail/sidebar.tsx | 20 +- .../properties/all-properties.tsx | 15 +- .../with-display-properties-HOC.tsx | 14 +- .../spreadsheet/columns/due-date-column.tsx | 9 +- .../spreadsheet/issue-column.tsx | 2 +- .../spreadsheet/spreadsheet-header-column.tsx | 2 +- web/components/issues/issue-modal/form.tsx | 34 +- .../issues/peek-overview/properties.tsx | 28 +- web/components/modules/form.tsx | 90 ++--- web/components/modules/sidebar.tsx | 225 +++-------- .../select-snooze-till-modal.tsx | 23 +- web/components/ui/datepicker.tsx | 69 ---- web/components/ui/index.ts | 2 - web/components/ui/range-datepicker.tsx | 65 ---- web/constants/issue.ts | 8 +- web/constants/spreadsheet.ts | 6 +- web/helpers/date-time.helper.ts | 23 +- web/package.json | 3 +- web/pages/_app.tsx | 2 +- web/styles/react-datepicker.css | 142 ------- web/styles/react-day-picker.css | 368 ++++++++++++++++++ yarn.lock | 40 +- 32 files changed, 1018 insertions(+), 998 deletions(-) create mode 100644 web/components/dropdowns/date-range.tsx delete mode 100644 web/components/ui/datepicker.tsx delete mode 100644 web/components/ui/range-datepicker.tsx delete mode 100644 web/styles/react-datepicker.css create mode 100644 web/styles/react-day-picker.css diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index ae7717b39..77753e64d 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -1,11 +1,10 @@ import { useState } from "react"; import { add } from "date-fns"; import { Controller, useForm } from "react-hook-form"; +import { DateDropdown } from "components/dropdowns"; import { Calendar } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; -// components -import { CustomDatePicker } from "components/ui"; // ui import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui"; // helpers @@ -167,7 +166,7 @@ export const CreateApiTokenForm: React.FC = (props) => { @@ -194,20 +193,13 @@ export const CreateApiTokenForm: React.FC = (props) => { }} /> {watch("expired_at") === "custom" && ( - setCustomDate(date ? new Date(date) : null)} + onChange={(date) => setCustomDate(date)} minDate={tomorrow} - customInput={ -
- - {customDate ? renderFormattedDate(customDate) : "Set date"} -
- } + icon={} + buttonVariant="border-with-text" + placeholder="Set date" disabled={neverExpires} /> )} diff --git a/web/components/core/filters/date-filter-modal.tsx b/web/components/core/filters/date-filter-modal.tsx index 9b460bf28..c5238ec1c 100644 --- a/web/components/core/filters/date-filter-modal.tsx +++ b/web/components/core/filters/date-filter-modal.tsx @@ -1,13 +1,12 @@ import { Fragment } from "react"; import { Controller, useForm } from "react-hook-form"; -import DatePicker from "react-datepicker"; +import { DayPicker } from "react-day-picker"; import { Dialog, Transition } from "@headlessui/react"; +import { X } from "lucide-react"; // components import { DateFilterSelect } from "./date-filter-select"; // ui import { Button } from "@plane/ui"; -// icons -import { X } from "lucide-react"; // helpers import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; @@ -46,9 +45,6 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false; - const nextDay = new Date(watch("date1")); - nextDay.setDate(nextDay.getDate() + 1); - return ( @@ -91,12 +87,15 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o control={control} name="date1" render={({ field: { value, onChange } }) => ( - onChange(val)} - dateFormat="dd-MM-yyyy" - calendarClassName="h-full" - inline + onChange(date)} + mode="single" + disabled={[ + { after: new Date(watch("date2")) } + ]} + className="border border-custom-border-200 p-3 rounded-md" /> )} /> @@ -105,13 +104,15 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o control={control} name="date2" render={({ field: { value, onChange } }) => ( - onChange(date)} + mode="single" + disabled={[ + { before: new Date(watch("date1")) } + ]} + className="border border-custom-border-200 p-3 rounded-md" /> )} /> diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 46aefcb0b..799d80438 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components -import { DateDropdown, ProjectDropdown } from "components/dropdowns"; +import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui import { Button, Input, TextArea } from "@plane/ui"; // helpers @@ -32,7 +32,6 @@ export const CycleForm: React.FC = (props) => { formState: { errors, isSubmitting, dirtyFields }, handleSubmit, control, - watch, reset, } = useForm({ defaultValues: { @@ -51,15 +50,6 @@ export const CycleForm: React.FC = (props) => { }); }, [data, reset]); - const startDate = watch("start_date"); - const endDate = watch("end_date"); - - const minDate = startDate ? new Date(startDate) : new Date(); - minDate.setDate(minDate.getDate() + 1); - - const maxDate = endDate ? new Date(endDate) : null; - maxDate?.setDate(maxDate.getDate() - 1); - return (
handleFormSubmit(formData, dirtyFields))}>
@@ -132,39 +122,37 @@ export const CycleForm: React.FC = (props) => {
-
- ( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Start date" - minDate={new Date()} - maxDate={maxDate ?? undefined} - tabIndex={3} - /> -
- )} - /> -
( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="End date" - minDate={minDate} - tabIndex={4} - /> -
+ name="start_date" + render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => ( + ( + { + onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); + onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); + }} + placeholder={{ + from: "Start date", + to: "End date", + }} + hideIcon={{ + to: true, + }} + tabIndex={3} + /> + )} + /> )} />
@@ -172,10 +160,10 @@ export const CycleForm: React.FC = (props) => {
- -
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index c825feb37..20605a90c 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { useForm } from "react-hook-form"; -import { Disclosure, Popover, Transition } from "@headlessui/react"; +import { Controller, useForm } from "react-hook-form"; +import { Disclosure, Transition } from "@headlessui/react"; import isEmpty from "lodash/isEmpty"; // services import { CycleService } from "services/cycle.service"; @@ -14,27 +14,12 @@ import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; // ui -import { CustomRangeDatePicker } from "components/ui"; import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; // icons -import { - ChevronDown, - LinkIcon, - Trash2, - UserCircle2, - AlertCircle, - ChevronRight, - CalendarCheck2, - CalendarClock, -} from "lucide-react"; +import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; -import { - findHowManyDaysLeft, - isDateGreaterThanToday, - renderFormattedPayloadDate, - renderFormattedDate, -} from "helpers/date-time.helper"; +import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { ICycle } from "@plane/types"; // constants @@ -42,6 +27,7 @@ import { EUserWorkspaceRoles } from "constants/workspace"; import { CYCLE_UPDATED } from "constants/event-tracker"; // fetch-keys import { CYCLE_STATUS } from "constants/cycle"; +import { DateRangeDropdown } from "components/dropdowns"; type Props = { cycleId: string; @@ -61,9 +47,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycleId, handleClose } = props; // states const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - // refs - const startDateButtonRef = useRef(null); - const endDateButtonRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; @@ -74,13 +57,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { } = useUser(); const { getCycleById, updateCycleDetails } = useCycle(); const { getUserDetails } = useMember(); - + // derived values const cycleDetails = getCycleById(cycleId); const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; - + // toast alert const { setToastAlert } = useToast(); - - const { setValue, reset, watch } = useForm({ + // form info + const { control, reset } = useForm({ defaultValues, }); @@ -145,160 +128,38 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { } }; - const handleStartDateChange = async (date: string) => { - setValue("start_date", date); + const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => { + if (!startDate || !endDate) return; - if (!watch("end_date") || watch("end_date") === "") endDateButtonRef.current?.click(); + let isDateValid = false; - if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") { - if (!isDateGreaterThanToday(`${watch("end_date")}`)) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Unable to create cycle in past date. Please enter a valid date.", - }); - reset({ ...cycleDetails }); - return; - } + const payload = { + start_date: renderFormattedPayloadDate(startDate), + end_date: renderFormattedPayloadDate(endDate), + }; - if (cycleDetails?.start_date && cycleDetails?.end_date) { - const isDateValidForExistingCycle = await dateChecker({ - start_date: `${watch("start_date")}`, - end_date: `${watch("end_date")}`, - cycle_id: cycleDetails.id, - }); - - if (isDateValidForExistingCycle) { - submitChanges( - { - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }, - "start_date" - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle updated successfully.", - }); - } else { - setToastAlert({ - type: "error", - title: "Error!", - message: - "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - }); - } - - reset({ ...cycleDetails }); - return; - } - - const isDateValid = await dateChecker({ - start_date: `${watch("start_date")}`, - end_date: `${watch("end_date")}`, + if (cycleDetails && cycleDetails.start_date && cycleDetails.end_date) + isDateValid = await dateChecker({ + ...payload, + cycle_id: cycleDetails.id, }); + else isDateValid = await dateChecker(payload); - if (isDateValid) { - submitChanges( - { - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }, - "start_date" - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle updated successfully.", - }); - } else { - setToastAlert({ - type: "error", - title: "Error!", - message: - "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - }); - reset({ ...cycleDetails }); - } - } - }; - - const handleEndDateChange = async (date: string) => { - setValue("end_date", date); - - if (!watch("start_date") || watch("start_date") === "") startDateButtonRef.current?.click(); - - if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") { - if (!isDateGreaterThanToday(`${watch("end_date")}`)) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Unable to create cycle in past date. Please enter a valid date.", - }); - reset({ ...cycleDetails }); - return; - } - - if (cycleDetails?.start_date && cycleDetails?.end_date) { - const isDateValidForExistingCycle = await dateChecker({ - start_date: `${watch("start_date")}`, - end_date: `${watch("end_date")}`, - cycle_id: cycleDetails.id, - }); - - if (isDateValidForExistingCycle) { - submitChanges( - { - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }, - "end_date" - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle updated successfully.", - }); - } else { - setToastAlert({ - type: "error", - title: "Error!", - message: - "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - }); - } - reset({ ...cycleDetails }); - return; - } - - const isDateValid = await dateChecker({ - start_date: `${watch("start_date")}`, - end_date: `${watch("end_date")}`, + if (isDateValid) { + submitChanges(payload, "date_range"); + setToastAlert({ + type: "success", + title: "Success!", + message: "Cycle updated successfully.", }); - - if (isDateValid) { - submitChanges( - { - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }, - "end_date" - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle updated successfully.", - }); - } else { - setToastAlert({ - type: "error", - title: "Error!", - message: - "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - }); - reset({ ...cycleDetails }); - } + } else { + setToastAlert({ + type: "error", + title: "Error!", + message: + "You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.", + }); + reset({ ...cycleDetails }); } }; @@ -351,9 +212,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { ); - const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? ""); - const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? ""); - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const issueCount = @@ -440,125 +298,52 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
+
- Start date + Date range
-
- - {({ close }) => ( - <> - - - {renderFormattedDate(startDate) ?? "No date selected"} - - - - - - { - if (val) { - setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON"); - handleStartDateChange(val); - close(); - } - }} - startDate={watch("start_date") ?? watch("end_date") ?? null} - endDate={watch("end_date") ?? watch("start_date") ?? null} - maxDate={new Date(`${watch("end_date")}`)} - selectsStart={watch("end_date") ? true : false} - /> - - - +
+ ( + ( + { + onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); + onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); + handleDateChange(val?.from, val?.to); + }} + placeholder={{ + from: "Start date", + to: "End date", + }} + required={cycleDetails.status !== "draft"} + /> + )} + /> )} - + />
-
- - Target date -
-
- - {({ close }) => ( - <> - - - {renderFormattedDate(endDate) ?? "No date selected"} - - - - - - { - if (val) { - setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON"); - handleEndDateChange(val); - close(); - } - }} - startDate={watch("start_date") ?? watch("end_date") ?? null} - endDate={watch("end_date") ?? watch("start_date") ?? null} - minDate={new Date(`${watch("start_date")}`)} - selectsEnd={watch("start_date") ? true : false} - /> - - - - )} - -
-
- -
-
+
Lead
-
+
{cycleOwnerDetails?.display_name} @@ -567,11 +352,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
+
Issues
-
+
{issueCount}
diff --git a/web/components/dropdowns/date-range.tsx b/web/components/dropdowns/date-range.tsx new file mode 100644 index 000000000..d3ef691b9 --- /dev/null +++ b/web/components/dropdowns/date-range.tsx @@ -0,0 +1,261 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { DateRange, DayPicker, Matcher } from "react-day-picker"; +import { ArrowRight, CalendarDays } from "lucide-react"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +// components +import { DropdownButton } from "./buttons"; +// ui +import { Button } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +// types +import { TButtonVariants } from "./types"; + +type Props = { + applyButtonText?: string; + bothRequired?: boolean; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonFromDateClassName?: string; + buttonToDateClassName?: string; + buttonVariant: TButtonVariants; + cancelButtonText?: string; + className?: string; + disabled?: boolean; + hideIcon?: { + from?: boolean; + to?: boolean; + }; + icon?: React.ReactNode; + minDate?: Date; + maxDate?: Date; + onSelect: (range: DateRange | undefined) => void; + placeholder?: { + from?: string; + to?: string; + }; + placement?: Placement; + required?: boolean; + showTooltip?: boolean; + tabIndex?: number; + value: { + from: Date | undefined; + to: Date | undefined; + }; +}; + +export const DateRangeDropdown: React.FC = (props) => { + const { + applyButtonText = "Apply changes", + bothRequired = true, + buttonClassName, + buttonContainerClassName, + buttonFromDateClassName, + buttonToDateClassName, + buttonVariant, + cancelButtonText = "Cancel", + className, + disabled = false, + hideIcon = { + from: true, + to: true, + }, + icon = , + minDate, + maxDate, + onSelect, + placeholder = { + from: "Add date", + to: "Add date", + }, + placement, + required = false, + showTooltip = false, + tabIndex, + value, + } = props; + // states + const [isOpen, setIsOpen] = useState(false); + const [dateRange, setDateRange] = useState(value); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const onOpen = () => { + if (referenceElement) referenceElement.focus(); + }; + + const handleClose = () => { + if (!isOpen) return; + setIsOpen(false); + setDateRange({ + from: value.from, + to: value.to, + }); + if (referenceElement) referenceElement.blur(); + }; + + const toggleDropdown = () => { + if (!isOpen) onOpen(); + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); + + const disabledDays: Matcher[] = []; + if (minDate) disabledDays.push({ before: minDate }); + if (maxDate) disabledDays.push({ after: maxDate }); + + useEffect(() => { + setDateRange(value); + }, [value]); + + return ( + { + if (e.key === "Enter") { + if (!isOpen) handleKeyDown(e); + } else handleKeyDown(e); + }} + > + + + + {isOpen && ( + +
+ { + // if both the dates are not required, immediately call onSelect + if (!bothRequired) onSelect(val); + setDateRange({ + from: val?.from ?? undefined, + to: val?.to ?? undefined, + }); + }} + mode="range" + disabled={disabledDays} + showOutsideDays + initialFocus + footer={ + bothRequired && ( +
+
+ + +
+ ) + } + /> +
+ + )} + + ); +}; diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 2603b3eb2..04c7d6948 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; -import DatePicker from "react-datepicker"; +import { DayPicker, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; import { CalendarDays, X } from "lucide-react"; // hooks @@ -50,6 +50,7 @@ export const DateDropdown: React.FC = (props) => { tabIndex, value, } = props; + // states const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); @@ -102,18 +103,25 @@ export const DateDropdown: React.FC = (props) => { useOutsideClickDetector(dropdownRef, handleClose); + const disabledDays: Matcher[] = []; + if (minDate) disabledDays.push({ before: minDate }); + if (maxDate) disabledDays.push({ after: maxDate }); + return ( { + if (e.key === "Enter") { + if (!isOpen) handleKeyDown(e); + } else handleKeyDown(e); + }} disabled={disabled} > } placement="bottom-start" - tabIndex={15} + tabIndex={14} > {watch("parent_id") ? ( <> @@ -661,7 +653,7 @@ export const IssueFormRoot: FC = observer((props) => { onKeyDown={(e) => { if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); }} - tabIndex={16} + tabIndex={15} >
{}} size="sm" /> @@ -669,7 +661,7 @@ export const IssueFormRoot: FC = observer((props) => { Create more
- @@ -681,7 +673,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))} - tabIndex={18} + tabIndex={17} > {isSubmitting ? "Moving" : "Move from draft"} @@ -691,7 +683,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit(data, true))} - tabIndex={18} + tabIndex={17} > {isSubmitting ? "Saving" : "Save as draft"} @@ -699,7 +691,7 @@ export const IssueFormRoot: FC = observer((props) => { )} -
diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 10bba9f11..c669c0349 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -1,16 +1,7 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { - Signal, - Tag, - Triangle, - LayoutPanelTop, - CircleDot, - CopyPlus, - XCircle, - CalendarClock, - CalendarCheck2, -} from "lucide-react"; +import { differenceInCalendarDays } from "date-fns"; +import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react"; // hooks import { useIssueDetail, useProject } from "hooks/store"; // ui icons @@ -33,6 +24,8 @@ import { } from "components/dropdowns"; // components import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// helpers +import { cn } from "helpers/common.helper"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -61,6 +54,8 @@ export const PeekOverviewProperties: FC = observer((pro const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); + const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; + return (
Properties
@@ -129,7 +124,7 @@ export const PeekOverviewProperties: FC = observer((pro {/* start date */}
- + Start date
= observer((pro {/* due date */}
- + Due date
= observer((pro disabled={disabled} className="w-3/4 flex-grow group" buttonContainerClassName="w-full text-left" - buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} + buttonClassName={cn("text-sm", { + "text-custom-text-400": !issue.target_date, + "text-red-500": targetDateDistance <= 0, + })} hideIcon - clearIconClassName="h-3 w-3 hidden group-hover:inline" + clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100" // TODO: add this logic // showPlaceholderIcon /> diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index a6f04ec54..191476357 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components import { ModuleStatusSelect } from "components/modules"; -import { DateDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns"; +import { DateRangeDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns"; // ui import { Button, Input, TextArea } from "@plane/ui"; // helpers @@ -27,18 +27,12 @@ const defaultValues: Partial = { member_ids: [], }; -export const ModuleForm: React.FC = ({ - handleFormSubmit, - handleClose, - status, - projectId, - setActiveProject, - data, -}) => { +export const ModuleForm: React.FC = (props) => { + const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props; + // form info const { formState: { errors, isSubmitting, dirtyFields }, handleSubmit, - watch, control, reset, } = useForm({ @@ -67,15 +61,6 @@ export const ModuleForm: React.FC = ({ }); }, [data, reset]); - const startDate = watch("start_date"); - const targetDate = watch("target_date"); - - const minDate = startDate ? new Date(startDate) : null; - minDate?.setDate(minDate.getDate()); - - const maxDate = targetDate ? new Date(targetDate) : null; - maxDate?.setDate(maxDate.getDate()); - return (
@@ -152,36 +137,39 @@ export const ModuleForm: React.FC = ({ ( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Start date" - maxDate={maxDate ?? undefined} - tabIndex={3} - /> -
+ render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => ( + ( + { + onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); + onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); + }} + placeholder={{ + from: "Start date", + to: "End date", + }} + hideIcon={{ + to: true, + }} + tabIndex={3} + /> + )} + /> )} /> - ( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Target date" - minDate={minDate ?? undefined} - tabIndex={4} - /> -
- )} - /> - +
+ +
= ({ multiple={false} buttonVariant="border-with-text" placeholder="Lead" - tabIndex={6} + tabIndex={5} />
)} @@ -212,7 +200,7 @@ export const ModuleForm: React.FC = ({ buttonVariant={value && value.length > 0 ? "transparent-without-text" : "border-with-text"} buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""} placeholder="Members" - tabIndex={7} + tabIndex={6} />
)} @@ -221,10 +209,10 @@ export const ModuleForm: React.FC = ({
- -
diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index b37875386..de31769f1 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,11 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; -import { Disclosure, Popover, Transition } from "@headlessui/react"; +import { Disclosure, Transition } from "@headlessui/react"; import { AlertCircle, - CalendarCheck2, CalendarClock, ChevronDown, ChevronRight, @@ -22,12 +21,11 @@ import useToast from "hooks/use-toast"; import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; -import { ProjectMemberDropdown } from "components/dropdowns"; +import { DateRangeDropdown, ProjectMemberDropdown } from "components/dropdowns"; // ui -import { CustomRangeDatePicker } from "components/ui"; import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui"; // helpers -import { isDateGreaterThanToday, renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; @@ -56,9 +54,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); - // refs - const startDateButtonRef = useRef(null); - const endDateButtonRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; @@ -72,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { setToastAlert } = useToast(); - const { setValue, watch, reset, control } = useForm({ + const { reset, control } = useForm({ defaultValues, }); @@ -85,7 +80,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" }, }); }) - .catch((_) => { + .catch(() => { captureModuleEvent({ eventName: MODULE_UPDATED, payload: { ...data, state: "FAILED" }, @@ -187,40 +182,16 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { }); }; - const handleStartDateChange = async (date: string) => { - setValue("start_date", date); - - if (!watch("target_date") || watch("target_date") === "") endDateButtonRef.current?.click(); - - if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - target_date: renderFormattedPayloadDate(`${watch("target_date")}`), - }); - setToastAlert({ - type: "success", - title: "Success!", - message: "Module updated successfully.", - }); - } - }; - - const handleEndDateChange = async (date: string) => { - setValue("target_date", date); - - if (!watch("start_date") || watch("start_date") === "") endDateButtonRef.current?.click(); - - if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { - submitChanges({ - target_date: renderFormattedPayloadDate(`${watch("target_date")}`), - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - }); - setToastAlert({ - type: "success", - title: "Success!", - message: "Module updated successfully.", - }); - } + const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => { + submitChanges({ + start_date: startDate ? renderFormattedPayloadDate(startDate) : null, + target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null, + }); + setToastAlert({ + type: "success", + title: "Success!", + message: "Module updated successfully.", + }); }; useEffect(() => { @@ -257,9 +228,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { ); - const startDate = new Date(watch("start_date") ?? moduleDetails.start_date ?? ""); - const endDate = new Date(watch("target_date") ?? moduleDetails.target_date ?? ""); - const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); const issueCount = @@ -319,7 +287,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { ( + render={({ field: { value } }) => ( = observer((props) => { )} -
-
-
- - - Start date -
-
- - {({ close }) => ( - <> - - - {renderFormattedDate(startDate) ?? "No date selected"} - - - - - - { - if (val) { - handleStartDateChange(val); - close(); - } - }} - startDate={watch("start_date") ?? watch("target_date") ?? null} - endDate={watch("target_date") ?? watch("start_date") ?? null} - maxDate={new Date(`${watch("target_date")}`)} - selectsStart={watch("target_date") ? true : false} - /> - - - - )} - -
+
+
+ + Date range
- -
-
- - Target date -
-
- - {({ close }) => ( - <> - - - {renderFormattedDate(endDate) ?? "No date selected"} - - - - - - { - if (val) { - handleEndDateChange(val); - close(); - } - }} - startDate={watch("start_date") ?? watch("target_date") ?? null} - endDate={watch("target_date") ?? watch("start_date") ?? null} - minDate={new Date(`${watch("start_date")}`)} - selectsEnd={watch("start_date") ? true : false} - /> - - - - )} - -
+
+ ( + ( + { + onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); + onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); + handleDateChange(val?.from, val?.to); + }} + placeholder={{ + from: "Start date", + to: "Target date", + }} + /> + )} + /> + )} + />
@@ -484,7 +376,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
-
+
Lead
@@ -492,7 +384,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="lead_id" render={({ field: { value } }) => ( -
+
{ @@ -508,7 +400,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { />
-
+
Members
@@ -516,7 +408,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="member_ids" render={({ field: { value } }) => ( -
+
{ @@ -532,13 +424,12 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { )} />
-
-
+
Issues
-
+
{issueCount}
diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index 2ad4b0ef2..f89b3a963 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -1,6 +1,7 @@ import { Fragment, FC } from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; +import { DateDropdown } from "components/dropdowns"; import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; // constants @@ -9,7 +10,6 @@ import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification" import useToast from "hooks/use-toast"; // ui import { Button, CustomSelect } from "@plane/ui"; -import { CustomDatePicker } from "components/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -169,17 +169,18 @@ export const SnoozeNotificationModal: FC = (props) => { control={control} rules={{ required: "Please select a date" }} render={({ field: { value, onChange } }) => ( - { setValue("time", null); onChange(val); }} - className="w-full rounded-md border border-custom-border-300 bg-custom-background-100 px-3 py-2 !text-sm text-custom-text-100 placeholder:!text-custom-text-400 focus:outline-none" - wrapperClassName="w-full" - noBorder minDate={new Date()} + buttonVariant="border-with-text" + buttonContainerClassName="w-full text-left" + buttonClassName="border-custom-border-300 px-3 py-2.5" + hideIcon /> )} /> @@ -213,11 +214,10 @@ export const SnoozeNotificationModal: FC = (props) => { onClick={() => { setValue("period", "AM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ - watch("period") === "AM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > AM
@@ -225,11 +225,10 @@ export const SnoozeNotificationModal: FC = (props) => { onClick={() => { setValue("period", "PM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ - watch("period") === "PM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > PM
diff --git a/web/components/ui/datepicker.tsx b/web/components/ui/datepicker.tsx deleted file mode 100644 index ef0a6b6e0..000000000 --- a/web/components/ui/datepicker.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// react-datepicker -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -// helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; - -type Props = { - renderAs?: "input" | "button"; - value: Date | string | null | undefined; - onChange: (val: string | null) => void; - handleOnOpen?: () => void; - handleOnClose?: () => void; - customInput?: React.ReactNode; - placeholder?: string; - displayShortForm?: boolean; - error?: boolean; - noBorder?: boolean; - wrapperClassName?: string; - className?: string; - isClearable?: boolean; - disabled?: boolean; - maxDate?: Date; - minDate?: Date; -}; - -export const CustomDatePicker: React.FC = ({ - renderAs = "button", - value, - onChange, - handleOnOpen, - handleOnClose, - placeholder = "Select date", - error = false, - noBorder = false, - wrapperClassName = "", - className = "", - isClearable = true, - disabled = false, - customInput, - maxDate, - minDate, -}) => ( - { - if (!val) onChange(null); - else onChange(renderFormattedPayloadDate(val)); - }} - onCalendarOpen={handleOnOpen} - onCalendarClose={handleOnClose} - wrapperClassName={wrapperClassName} - customInput={customInput} - className={`${ - renderAs === "input" - ? "block px-2 py-2 text-sm focus:outline-none" - : renderAs === "button" - ? `px-2 py-1 text-xs shadow-sm ${disabled ? "" : "hover:bg-custom-background-80"} duration-300` - : "" - } ${error ? "border-red-500 bg-red-100" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"} ${ - noBorder ? "" : "border border-custom-border-200" - } w-full rounded-md caret-transparent outline-none ${className}`} - dateFormat="MMM dd, yyyy" - isClearable={Boolean(isClearable && !disabled)} - disabled={disabled} - maxDate={maxDate} - minDate={minDate} - /> -); diff --git a/web/components/ui/index.ts b/web/components/ui/index.ts index 3a867b18c..87a9686b6 100644 --- a/web/components/ui/index.ts +++ b/web/components/ui/index.ts @@ -1,10 +1,8 @@ export * from "./graphs"; -export * from "./datepicker"; export * from "./empty-space"; export * from "./labels-list"; export * from "./multi-level-dropdown"; export * from "./markdown-to-component"; export * from "./integration-and-import-export-banner"; -export * from "./range-datepicker"; export * from "./profile-empty-state"; export * from "./loader"; diff --git a/web/components/ui/range-datepicker.tsx b/web/components/ui/range-datepicker.tsx deleted file mode 100644 index 55760ae01..000000000 --- a/web/components/ui/range-datepicker.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// react-datepicker -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -// helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; - -type Props = { - renderAs?: "input" | "button"; - value: Date | string | null | undefined; - onChange: (val: string | null) => void; - error?: boolean; - className?: string; - isClearable?: boolean; - disabled?: boolean; - startDate: string | null; - endDate: string | null; - selectsStart?: boolean; - selectsEnd?: boolean; - minDate?: Date | null | undefined; - maxDate?: Date | null | undefined; -}; - -export const CustomRangeDatePicker: React.FC = ({ - renderAs = "button", - value, - onChange, - error = false, - className = "", - disabled = false, - startDate, - endDate, - selectsStart = false, - selectsEnd = false, - minDate = null, - maxDate = null, -}) => ( - { - if (!val) onChange(null); - else onChange(renderFormattedPayloadDate(val)); - }} - className={`${ - renderAs === "input" - ? "block px-3 py-2 text-sm focus:outline-none" - : renderAs === "button" - ? `px-3 py-1 text-xs shadow-sm ${ - disabled ? "" : "hover:bg-custom-background-80" - } duration-300 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary` - : "" - } ${error ? "border-red-500 bg-red-100" : ""} ${ - disabled ? "cursor-not-allowed" : "cursor-pointer" - } w-full rounded-md border border-custom-border-200 bg-transparent caret-transparent ${className}`} - dateFormat="dd-MM-yyyy" - disabled={disabled} - selectsStart={selectsStart} - selectsEnd={selectsEnd} - startDate={startDate ? new Date(startDate) : new Date()} - endDate={endDate ? new Date(endDate) : new Date()} - minDate={minDate} - maxDate={maxDate} - shouldCloseOnSelect - inline - /> -); diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 94e57f8d9..4bb6bcec0 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -106,14 +106,14 @@ export const ISSUE_DISPLAY_PROPERTIES: { title: string; }[] = [ { key: "assignee", title: "Assignee" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, + { key: "start_date", title: "Start date" }, + { key: "due_date", title: "Due date" }, { key: "key", title: "ID" }, { key: "labels", title: "Labels" }, { key: "priority", title: "Priority" }, { key: "state", title: "State" }, - { key: "sub_issue_count", title: "Sub Issue Count" }, - { key: "attachment_count", title: "Attachment Count" }, + { key: "sub_issue_count", title: "Sub issue count" }, + { key: "attachment_count", title: "Attachment count" }, { key: "link", title: "Link" }, { key: "estimate", title: "Estimate" }, { key: "modules", title: "Modules" }, diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index 8d87fb132..465449047 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,6 +1,6 @@ import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; -import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react"; +import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react"; import { FC } from "react"; import { ISvgIcons } from "@plane/ui/src/icons/type"; import { @@ -60,7 +60,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "target_date", descendingOrderTitle: "Old", - icon: CalendarCheck, + icon: CalendarDays, Column: SpreadsheetDueDateColumn, }, estimate: { @@ -114,7 +114,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "start_date", descendingOrderTitle: "Old", - icon: CalendarClock, + icon: CalendarDays, Column: SpreadsheetStartDateColumn, }, state: { diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index b629e60ec..5423d4e92 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -1,4 +1,5 @@ -import { differenceInDays, format, formatDistanceToNow, isAfter, isValid, parseISO } from "date-fns"; +import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns"; +import { isNil } from "lodash"; // Format Date Helpers /** @@ -172,3 +173,23 @@ export const getWeekNumberOfDate = (date: Date): number => { const weekNumber = Math.ceil((days + 1) / 7); return weekNumber; }; + +/** + * @returns {boolean} boolean value depending on whether the dates are equal + * @description Returns boolean value depending on whether the dates are equal + * @param date1 + * @param date2 + * @example checkIfDatesAreEqual("2024-01-01", "2024-01-01") // true + * @example checkIfDatesAreEqual("2024-01-01", "2024-01-02") // false + */ +export const checkIfDatesAreEqual = ( + date1: Date | string | null | undefined, + date2: Date | string | null | undefined +): boolean => { + if (isNil(date1) && isNil(date2)) return true; + if (isNil(date1) || isNil(date2)) return false; + + const parsedDate1 = new Date(date1); + const parsedDate2 = new Date(date2); + return isEqual(parsedDate1, parsedDate2); +}; diff --git a/web/package.json b/web/package.json index 9670c2833..482472672 100644 --- a/web/package.json +++ b/web/package.json @@ -48,7 +48,7 @@ "posthog-js": "^1.105.0", "react": "18.2.0", "react-color": "^2.19.3", - "react-datepicker": "^4.8.0", + "react-day-picker": "^8.10.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", @@ -67,7 +67,6 @@ "@types/nprogress": "^0.2.0", "@types/react": "^18.2.42", "@types/react-color": "^3.0.6", - "@types/react-datepicker": "^4.8.0", "@types/react-dom": "^18.2.17", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.48.2", diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index a53ae80ab..75023d36e 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -5,7 +5,7 @@ import { AppProps } from "next/app"; import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; -import "styles/react-datepicker.css"; +import "styles/react-day-picker.css"; // constants import { SITE_TITLE } from "constants/seo-variables"; // mobx store provider diff --git a/web/styles/react-datepicker.css b/web/styles/react-datepicker.css deleted file mode 100644 index 7eaf3377e..000000000 --- a/web/styles/react-datepicker.css +++ /dev/null @@ -1,142 +0,0 @@ -.react-datepicker-wrapper input::placeholder { - color: rgba(var(--color-text-200)); - opacity: 1; -} - -.react-datepicker-wrapper input:-ms-input-placeholder { - color: rgba(var(--color-text-200)); -} - -.react-datepicker-wrapper .react-datepicker__close-icon::after { - background: transparent; - color: rgba(var(--color-text-200)); -} - -.react-datepicker-popper { - z-index: 30 !important; -} - -.react-datepicker-wrapper { - position: relative; - background-color: rgba(var(--color-background-100)) !important; -} - -.react-datepicker-wrapper, -.react-datepicker__input-container { - border-radius: 0.375rem; -} - -.react-datepicker { - font-family: "Inter" !important; - background-color: rgba(var(--color-background-100)) !important; - border: 0.5px solid rgba(var(--color-border-300)) !important; -} - -.react-datepicker__month-container { - width: 300px; - background-color: rgba(var(--color-background-100)) !important; - color: rgba(var(--color-text-100)) !important; - border-radius: 10px !important; - /* border: 1px solid rgba(var(--color-background-80)) !important; */ -} - -.react-datepicker__header { - border-radius: 10px !important; - background-color: rgba(var(--color-background-100)) !important; - border: none !important; -} - -.react-datepicker__navigation { - line-height: 0.78; -} - -.react-datepicker__triangle { - border-color: rgba(var(--color-background-100)) transparent transparent transparent !important; -} - -.react-datepicker__triangle:before { - border-bottom-color: rgba(var(--color-background-80)) !important; -} -.react-datepicker__triangle:after { - border-bottom-color: rgba(var(--color-background-100)) !important; -} - -.react-datepicker__current-month { - font-weight: 500 !important; - color: rgba(var(--color-text-100)) !important; -} - -.react-datepicker__month { - border-collapse: collapse; - color: rgba(var(--color-text-100)) !important; -} - -.react-datepicker__day-names { - margin-top: 10px; - margin-left: 14px; - width: 280px; - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 0; -} - -.react-datepicker__day-name { - color: rgba(var(--color-text-200)) !important; -} - -.react-datepicker__week { - display: grid; - grid-template-columns: repeat(7, 1fr); - margin-left: 8px; -} - -.react-datepicker__day { - color: rgba(var(--color-text-100)) !important; -} - -.react-datepicker__day { - border-radius: 50% !important; - transition: all 0.15s ease-in-out; -} - -.react-datepicker__day:hover { - background-color: rgba(var(--color-background-80)) !important; - color: rgba(var(--color-text-100)) !important; -} - -.react-datepicker__day--selected, -.react-datepicker__day--selected:hover { - background-color: #216ba5 !important; - color: white !important; -} - -.react-datepicker__day--disabled, -.react-datepicker__day--disabled:hover { - background: transparent !important; - color: rgba(var(--color-text-400)) !important; - cursor: default; -} - -.react-datepicker__day--today { - font-weight: 800; -} - -.react-datepicker__day--highlighted { - background-color: rgba(var(--color-background-80)) !important; -} - -.react-datepicker__day--keyboard-selected { - background-color: #216ba5 !important; - color: white !important; -} - -.react-datepicker__day--in-range-start, -.react-datepicker__day--in-range-end { - background-color: rgba(var(--color-primary-100)) !important; - color: white !important; -} - -.react-datepicker__day--in-range:not(.react-datepicker__day--in-range-start):not(.react-datepicker__day--in-range-end) { - background-color: rgba(var(--color-primary-100)) !important; - color: white !important; -} diff --git a/web/styles/react-day-picker.css b/web/styles/react-day-picker.css new file mode 100644 index 000000000..4c24e9cf7 --- /dev/null +++ b/web/styles/react-day-picker.css @@ -0,0 +1,368 @@ +.rdp { + font-size: 12px; + + --rdp-cell-size: 40px; + /* Size of the day cells. */ + --rdp-caption-font-size: 1.15rem; + /* Font size for the caption labels. */ + --rdp-caption-navigation-size: 1.25rem; + /* Font size for the caption labels. */ + --rdp-accent-color: rgba(var(--color-primary-100)); + /* Accent color for the background of selected days. */ + --rdp-background-color: rgba(var(--color-primary-100), 0.2); + /* Background color for the hovered/focused elements. */ + --rdp-dark-background-color: rgba(var(--color-primary-300)); + /* Background color for the hovered/focused, already selected elements. */ + --rdp-outline: 2px solid var(--rdp-accent-color); + /* Outline border for focused elements */ + --rdp-selected-color: #ffffff; + /* Color of selected day text */ + + background: transparent; +} + +/* Hide elements for devices that are not screen readers */ +.rdp-vhidden { + box-sizing: border-box; + padding: 0; + margin: 0; + background: transparent; + border: 0; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + position: absolute !important; + top: 0; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + overflow: hidden !important; + clip: rect(1px, 1px, 1px, 1px) !important; + border: 0 !important; +} + +/* Buttons */ +.rdp-button_reset { + appearance: none; + position: relative; + margin: 0; + padding: 0; + cursor: default; + color: inherit; + background: none; + font: inherit; + + -moz-appearance: none; + -webkit-appearance: none; +} + +.rdp-button_reset:focus-visible { + /* Make sure to reset outline only when :focus-visible is supported */ + outline: none; +} + +.rdp-button { + border: 2px solid transparent; +} + +.rdp-button[disabled]:not(.rdp-day_selected) { + opacity: 0.25; +} + +.rdp-button:not([disabled]) { + cursor: pointer; +} + +.rdp-button:focus-visible:not([disabled]):not(.rdp-day_selected) { + color: inherit; + background-color: var(--rdp-background-color); +} + +.rdp-button:focus-visible:not([disabled]).rdp-day_selected:not(.rdp-day_range_middle) { + outline: var(--rdp-outline); + outline-offset: 2px; + background-color: var(--rdp-dark-background-color); + outline-width: thin; +} + +.rdp-button:hover:not([disabled]).rdp-day_selected { + background-color: var(--rdp-dark-background-color); +} + +.rdp-button:hover:not([disabled]):not(.rdp-day_selected) { + background-color: var(--rdp-background-color); +} + +.rdp-months { + display: flex; +} + +.rdp-month { + margin: 0 1em; +} + +.rdp-month:first-child { + margin-left: 0; +} + +.rdp-month:last-child { + margin-right: 0; +} + +.rdp-table { + margin: 0; + max-width: calc(var(--rdp-cell-size) * 7); + border-collapse: collapse; +} + +.rdp-with_weeknumber .rdp-table { + max-width: calc(var(--rdp-cell-size) * 8); + border-collapse: collapse; +} + +.rdp-caption { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0; + text-align: left; +} + +.rdp-multiple_months .rdp-caption { + position: relative; + display: block; + text-align: center; +} + +.rdp-caption_dropdowns { + position: relative; + display: inline-flex; +} + +.rdp-caption_label { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + margin: 0; + padding: 0 0.25em; + white-space: nowrap; + color: currentColor; + border: 0; + border: 2px solid transparent; + font-family: inherit; + font-size: var(--rdp-caption-font-size); + font-weight: 600; +} + +.rdp-nav { + white-space: nowrap; +} + +.rdp-multiple_months .rdp-caption_start .rdp-nav { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); +} + +.rdp-multiple_months .rdp-caption_end .rdp-nav { + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); +} + +.rdp-nav_button { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--rdp-caption-navigation-size); + height: var(--rdp-caption-navigation-size); + padding: 0.25em; + border-radius: 2px; +} + +.rdp-nav_button:hover, +.rdp-nav_button:focus-visible { + background-color: rgba(var(--color-background-80)) !important; +} + +/* ---------- */ +/* Dropdowns */ +/* ---------- */ + +.rdp-dropdown_year, +.rdp-dropdown_month { + position: relative; + display: inline-flex; + align-items: center; +} + +.rdp-dropdown { + appearance: none; + position: absolute; + z-index: 2; + top: 0; + bottom: 0; + left: 0; + width: 100%; + margin: 0; + padding: 0; + cursor: inherit; + opacity: 0; + border: none; + background-color: transparent; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.rdp-dropdown[disabled] { + opacity: unset; + color: unset; +} + +.rdp-dropdown:focus-visible:not([disabled])+.rdp-caption_label { + background-color: var(--rdp-background-color); + border: var(--rdp-outline); + border-radius: 6px; +} + +.rdp-dropdown_icon { + margin: 0 0 0 5px; +} + +.rdp-head { + border: 0; +} + +.rdp-head_row, +.rdp-row { + height: 100%; +} + +.rdp-head_cell { + vertical-align: middle; + font-size: 0.75em; + font-weight: 700; + text-align: center; + height: 100%; + height: var(--rdp-cell-size); + padding: 0; + text-transform: uppercase; +} + +.rdp-tbody { + border: 0; +} + +.rdp-tfoot { + margin: 0.5em; +} + +.rdp-cell { + width: var(--rdp-cell-size); + height: 100%; + height: var(--rdp-cell-size); + padding: 0; + text-align: center; +} + +.rdp-weeknumber { + font-size: 0.75em; +} + +.rdp-weeknumber, +.rdp-day { + display: flex; + overflow: hidden; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: var(--rdp-cell-size); + max-width: var(--rdp-cell-size); + height: var(--rdp-cell-size); + margin: 0; + border: 2px solid transparent; + border-radius: 100%; +} + +.rdp-day_today:not(.rdp-day_outside) { + position: relative; +} + +.rdp-day_today:not(.rdp-day_outside)::after { + content: ''; + position: absolute; + left: 50%; + bottom: 2px; + width: 0.5em; + height: 0.5em; + background-color: var(--rdp-background-color); + border-radius: 100%; + transform: translate(-50%, 0); +} + +.rdp-day_selected, +.rdp-day_selected:focus-visible, +.rdp-day_selected:hover { + color: var(--rdp-selected-color); + opacity: 1; + background-color: var(--rdp-accent-color); +} + +.rdp-day_outside:not(.rdp-day_selected) { + opacity: 0.5; +} + +.rdp-day_selected:focus-visible { + z-index: 1; +} + +td:has(.rdp-day_range_start), +td:has(.rdp-day_range_middle), +td:has(.rdp-day_range_end) { + position: relative; +} + +td:has(.rdp-day_range_start)::before, +td:has(.rdp-day_range_middle)::before, +td:has(.rdp-day_range_end)::before { + content: ''; + position: absolute; + background-color: var(--rdp-background-color); + top: 50%; + height: 100%; + width: 50%; + transform: translate(0, -50%); +} + +td:has(.rdp-day_range_start)::before { + left: 50%; +} + +td:has(.rdp-day_range_middle)::before { + left: 50%; + width: 100%; + transform: translate(-50%, -50%); +} + +td:has(.rdp-day_range_end)::before { + right: 50%; +} + +td:has(.rdp-day_range_start.rdp-day_range_end)::before { + display: none; +} + +.rdp-day_range_middle { + background-color: transparent; + color: inherit; +} + +.rdp-day_range_middle:hover, +.rdp-day_range_middle:focus-visible { + background-color: var(--rdp-background-color) !important; + color: inherit !important; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1f9b26965..32f0a8aac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1865,7 +1865,7 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@popperjs/core@^2.11.7", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0", "@popperjs/core@^2.9.2": +"@popperjs/core@^2.11.7", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== @@ -2791,16 +2791,6 @@ "@types/react" "*" "@types/reactcss" "*" -"@types/react-datepicker@^4.8.0": - version "4.19.4" - resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.19.4.tgz#07807187ead4c7025bc0039fe05eb5713298f3e4" - integrity sha512-HRD0LHTxBVe61LRJgTdPscbapLQl7+jI/7bxnPGpvzdJ/iXN9q7ucYv8HKULeIAN84O5LzFhwTMOkO4QnIUJaQ== - dependencies: - "@popperjs/core" "^2.9.2" - "@types/react" "*" - date-fns "^2.0.1" - react-popper "^2.2.5" - "@types/react-dom@^18.2.17": version "18.2.18" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" @@ -3617,7 +3607,7 @@ class-variance-authority@^0.7.0: dependencies: clsx "2.0.0" -classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2: +classnames@^2.3.1, classnames@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.0.tgz#9304d63d8d7135989e33e1ffb0bcc65178f92c2a" integrity sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA== @@ -3918,7 +3908,7 @@ dash-get@^1.0.2: resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c" integrity sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ== -date-fns@^2.0.1, date-fns@^2.30.0: +date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== @@ -7237,7 +7227,7 @@ progress@^2.0.0, progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7481,17 +7471,10 @@ react-css-styled@^1.1.9: css-styled "~1.0.8" framework-utils "^1.1.0" -react-datepicker@^4.8.0: - version "4.25.0" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.25.0.tgz#86b3ee8ac764bad1650046d0cf9280837bf6d845" - integrity sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg== - dependencies: - "@popperjs/core" "^2.11.8" - classnames "^2.2.6" - date-fns "^2.30.0" - prop-types "^15.7.2" - react-onclickoutside "^6.13.0" - react-popper "^2.3.0" +react-day-picker@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.0.tgz#729c5b9564967a924213978fb9c0751884a60595" + integrity sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg== react-dom@18.2.0, react-dom@^18.2.0: version "18.2.0" @@ -7575,11 +7558,6 @@ react-moveable@^0.54.2: react-css-styled "^1.1.9" react-selecto "^1.25.0" -react-onclickoutside@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" - integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A== - react-popper@^1.3.11: version "1.3.11" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.11.tgz#a2cc3f0a67b75b66cfa62d2c409f9dd1fcc71ffd" @@ -7593,7 +7571,7 @@ react-popper@^1.3.11: typed-styles "^0.0.7" warning "^4.0.2" -react-popper@^2.2.5, react-popper@^2.3.0: +react-popper@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==