From e1ec359e3ee74d15d78c4b6a8f739e3d395b201f Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Tue, 10 Dec 2024 14:46:04 +0100 Subject: [PATCH 01/11] feat(manager-dashboard): street project definitions --- manager-dashboard/app/Base/configs/projectTypes.ts | 2 ++ manager-dashboard/app/utils/common.tsx | 4 +++- manager-dashboard/app/views/NewProject/index.tsx | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/manager-dashboard/app/Base/configs/projectTypes.ts b/manager-dashboard/app/Base/configs/projectTypes.ts index fd8d2df8..e2f7f74e 100644 --- a/manager-dashboard/app/Base/configs/projectTypes.ts +++ b/manager-dashboard/app/Base/configs/projectTypes.ts @@ -3,6 +3,7 @@ import { PROJECT_TYPE_BUILD_AREA, PROJECT_TYPE_FOOTPRINT, PROJECT_TYPE_CHANGE_DETECTION, + PROJECT_TYPE_STREET, PROJECT_TYPE_COMPLETENESS, } from '#utils/common'; @@ -15,6 +16,7 @@ const mapswipeProjectTypeOptions: { { value: PROJECT_TYPE_BUILD_AREA, label: 'Find' }, { value: PROJECT_TYPE_FOOTPRINT, label: 'Validate' }, { value: PROJECT_TYPE_CHANGE_DETECTION, label: 'Compare' }, + { value: PROJECT_TYPE_STREET, label: 'Street' }, { value: PROJECT_TYPE_COMPLETENESS, label: 'Completeness' }, ]; diff --git a/manager-dashboard/app/utils/common.tsx b/manager-dashboard/app/utils/common.tsx index 571be709..1a37b3e8 100644 --- a/manager-dashboard/app/utils/common.tsx +++ b/manager-dashboard/app/utils/common.tsx @@ -65,8 +65,9 @@ export const PROJECT_TYPE_BUILD_AREA = 1; export const PROJECT_TYPE_FOOTPRINT = 2; export const PROJECT_TYPE_CHANGE_DETECTION = 3; export const PROJECT_TYPE_COMPLETENESS = 4; +export const PROJECT_TYPE_STREET = 7; -export type ProjectType = 1 | 2 | 3 | 4; +export type ProjectType = 1 | 2 | 3 | 4 | 7; export const projectTypeLabelMap: { [key in ProjectType]: string @@ -75,6 +76,7 @@ export const projectTypeLabelMap: { [PROJECT_TYPE_FOOTPRINT]: 'Validate', [PROJECT_TYPE_CHANGE_DETECTION]: 'Compare', [PROJECT_TYPE_COMPLETENESS]: 'Completeness', + [PROJECT_TYPE_STREET]: 'Street', }; export type IconKey = 'add-outline' diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index f63bbc06..6456527d 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -59,6 +59,7 @@ import { PROJECT_TYPE_FOOTPRINT, PROJECT_TYPE_COMPLETENESS, PROJECT_TYPE_CHANGE_DETECTION, + PROJECT_TYPE_STREET, formatProjectTopic, } from '#utils/common'; import { getValueFromFirebase } from '#utils/firebase'; From 0188ba128ac7996d5519828a7c82376b6aa952ba Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Tue, 10 Dec 2024 14:55:55 +0100 Subject: [PATCH 02/11] feat(manager-dashboard): invisible tile server input in street projects --- .../app/views/NewProject/index.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index 6456527d..8f932955 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -418,6 +418,11 @@ function NewProject(props: Props) { || projectSubmissionStatus === 'projectSubmit' ); + const tileServerVisible = value.projectType === PROJECT_TYPE_BUILD_AREA + || value.projectType === PROJECT_TYPE_FOOTPRINT + || value.projectType === PROJECT_TYPE_COMPLETENESS + || value.projectType === PROJECT_TYPE_CHANGE_DETECTION; + const tileServerBVisible = value.projectType === PROJECT_TYPE_CHANGE_DETECTION || value.projectType === PROJECT_TYPE_COMPLETENESS; @@ -662,17 +667,19 @@ function NewProject(props: Props) { /> - - - + {tileServerVisible && ( + + + + )} {tileServerBVisible && ( Date: Tue, 10 Dec 2024 15:26:41 +0100 Subject: [PATCH 03/11] feat(manager-dashboard): add custom options preview and project aoi geometry input to street --- manager-dashboard/app/views/NewProject/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index 8f932955..e1d86a2d 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -474,7 +474,8 @@ function NewProject(props: Props) { /> {( - value.projectType === PROJECT_TYPE_FOOTPRINT + (value.projectType === PROJECT_TYPE_FOOTPRINT + || value.projectType === PROJECT_TYPE_STREET) && customOptions && customOptions.length > 0 ) && ( @@ -532,7 +533,8 @@ function NewProject(props: Props) { )} {(value.projectType === PROJECT_TYPE_BUILD_AREA || value.projectType === PROJECT_TYPE_CHANGE_DETECTION - || value.projectType === PROJECT_TYPE_COMPLETENESS) && ( + || value.projectType === PROJECT_TYPE_COMPLETENESS + || value.projectType === PROJECT_TYPE_STREET) && ( From c4f43c99282aeb6e3020231bc714727768f59387 Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Tue, 10 Dec 2024 15:42:59 +0100 Subject: [PATCH 04/11] feat(manager-dashboard): set default groupSize for street --- manager-dashboard/app/views/NewProject/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manager-dashboard/app/views/NewProject/utils.ts b/manager-dashboard/app/views/NewProject/utils.ts index e607d3a1..49441147 100644 --- a/manager-dashboard/app/views/NewProject/utils.ts +++ b/manager-dashboard/app/views/NewProject/utils.ts @@ -34,6 +34,7 @@ import { PROJECT_TYPE_FOOTPRINT, PROJECT_TYPE_CHANGE_DETECTION, PROJECT_TYPE_COMPLETENESS, + PROJECT_TYPE_STREET, IconKey, } from '#utils/common'; @@ -541,7 +542,9 @@ export function getGroupSize(projectType: ProjectType | undefined) { return 120; } - if (projectType === PROJECT_TYPE_FOOTPRINT || projectType === PROJECT_TYPE_CHANGE_DETECTION) { + if (projectType === PROJECT_TYPE_FOOTPRINT + || projectType === PROJECT_TYPE_CHANGE_DETECTION + || projectType === PROJECT_TYPE_STREET) { return 25; } From f9ae7689f63f2c594bf271271c8e6aa5979fabf4 Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Tue, 10 Dec 2024 16:43:11 +0100 Subject: [PATCH 05/11] feat(manager-dashboard): add banner to alert that street projects are web only --- manager-dashboard/app/views/NewProject/index.tsx | 11 +++++++++++ manager-dashboard/app/views/NewProject/styles.css | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index e1d86a2d..065ed63b 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -50,6 +50,7 @@ import Button from '#components/Button'; import NonFieldError from '#components/NonFieldError'; import AnimatedSwipeIcon from '#components/AnimatedSwipeIcon'; import ExpandableContainer from '#components/ExpandableContainer'; +import AlertBanner from '#components/AlertBanner'; import { valueSelector, labelSelector, @@ -465,6 +466,16 @@ function NewProject(props: Props) { error={error?.projectType} disabled={submissionPending || testPending} /> + {value.projectType === PROJECT_TYPE_STREET && ( + +
+
+ Projects of this type are currently + only visible in the web app. +
+
+
+ )} Date: Tue, 10 Dec 2024 17:06:15 +0100 Subject: [PATCH 06/11] feat(manager-dashboard): add street specific inputs (wip) --- .../app/views/NewProject/index.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index 065ed63b..ba40ff36 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -707,6 +707,30 @@ function NewProject(props: Props) { />
)} + + {value.projectType === PROJECT_TYPE_STREET && ( + /* TODO: Add street project inputs for + startTimestamp, + endTimeStamp, + isPano, + organizationId, + samplingThreshold + */ + + + + )} + {error?.[nonFieldError] && (
{error?.[nonFieldError]} From 78c4908673831f16303723349afe7290f290ce08 Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Wed, 11 Dec 2024 13:43:58 +0100 Subject: [PATCH 07/11] feat(manager-dashboard): add validation for organizationId --- manager-dashboard/app/views/NewProject/utils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/manager-dashboard/app/views/NewProject/utils.ts b/manager-dashboard/app/views/NewProject/utils.ts index 49441147..823e9a32 100644 --- a/manager-dashboard/app/views/NewProject/utils.ts +++ b/manager-dashboard/app/views/NewProject/utils.ts @@ -76,6 +76,7 @@ export interface ProjectFormType { tileServer: TileServer; tileServerB?: TileServer; customOptions?: CustomOptionsForProject; + organizationId?: number; } export const PROJECT_INPUT_TYPE_UPLOAD = 'aoi_file'; @@ -273,6 +274,12 @@ export const projectFormSchema: ProjectFormSchema = { greaterThanCondition(0), ], }, + organizationId: { + validations: [ + integerCondition, + greaterThanCondition(0), + ], + }, }; baseSchema = addCondition( From 905dcde5dc97eed88094f10cc5df9c171115d262 Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Wed, 11 Dec 2024 18:58:11 +0100 Subject: [PATCH 08/11] feat(manager-dashboard): add mapillary image filter inputs (wip) --- .../app/views/NewProject/index.tsx | 23 ++++++++++++++++++- .../app/views/NewProject/utils.ts | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index ba40ff36..7bad14c4 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -51,6 +51,7 @@ import NonFieldError from '#components/NonFieldError'; import AnimatedSwipeIcon from '#components/AnimatedSwipeIcon'; import ExpandableContainer from '#components/ExpandableContainer'; import AlertBanner from '#components/AlertBanner'; +import Checkbox from '#components/Checkbox'; import { valueSelector, labelSelector, @@ -106,6 +107,7 @@ const defaultProjectFormValue: PartialProjectFormType = { // maxTasksPerUser: -1, inputType: PROJECT_INPUT_TYPE_UPLOAD, filter: FILTER_BUILDINGS, + isPano: false, }; interface Props { @@ -719,15 +721,34 @@ function NewProject(props: Props) { +
time range input
+
+ + +
)} diff --git a/manager-dashboard/app/views/NewProject/utils.ts b/manager-dashboard/app/views/NewProject/utils.ts index 823e9a32..90257d08 100644 --- a/manager-dashboard/app/views/NewProject/utils.ts +++ b/manager-dashboard/app/views/NewProject/utils.ts @@ -77,6 +77,8 @@ export interface ProjectFormType { tileServerB?: TileServer; customOptions?: CustomOptionsForProject; organizationId?: number; + isPano?: boolean; + samplingThreshold?: number; } export const PROJECT_INPUT_TYPE_UPLOAD = 'aoi_file'; @@ -280,6 +282,11 @@ export const projectFormSchema: ProjectFormSchema = { greaterThanCondition(0), ], }, + samplingThreshold: { + validation: [ + greaterThanCondition(0), + ], + }, }; baseSchema = addCondition( From e90ebe543823bffea3f1986e64a54ac3558fb69a Mon Sep 17 00:00:00 2001 From: Oliver Fritz Date: Thu, 12 Dec 2024 19:54:27 +0100 Subject: [PATCH 09/11] feat(manager-dashboard): complete mapillary image filter inputs --- manager-dashboard/app/Base/styles.css | 7 +- .../Calendar/CalendarDate/index.tsx | 68 +++ .../Calendar/CalendarDate/styles.css | 25 ++ .../app/components/Calendar/index.tsx | 292 ++++++++++++ .../app/components/Calendar/styles.css | 93 ++++ .../app/components/DateRangeInput/index.tsx | 420 ++++++++++++++++++ .../DateRangeInput/predefinedDateRange.ts | 181 ++++++++ .../app/components/DateRangeInput/styles.css | 85 ++++ manager-dashboard/app/utils/common.tsx | 14 + .../app/views/NewProject/index.tsx | 21 +- .../app/views/NewProject/utils.ts | 14 + 11 files changed, 1211 insertions(+), 9 deletions(-) create mode 100644 manager-dashboard/app/components/Calendar/CalendarDate/index.tsx create mode 100644 manager-dashboard/app/components/Calendar/CalendarDate/styles.css create mode 100644 manager-dashboard/app/components/Calendar/index.tsx create mode 100644 manager-dashboard/app/components/Calendar/styles.css create mode 100644 manager-dashboard/app/components/DateRangeInput/index.tsx create mode 100644 manager-dashboard/app/components/DateRangeInput/predefinedDateRange.ts create mode 100644 manager-dashboard/app/components/DateRangeInput/styles.css diff --git a/manager-dashboard/app/Base/styles.css b/manager-dashboard/app/Base/styles.css index 306f01ca..c746dc57 100644 --- a/manager-dashboard/app/Base/styles.css +++ b/manager-dashboard/app/Base/styles.css @@ -122,9 +122,14 @@ p { --line-height-relaxed: 1.625; --line-height-loose: 2; - --shadow-card: 0 2px 4px -2px var(--color-shadow); --duration-transition-medium: .2s; + --color-background-hover-light: rgba(0, 0, 0, .04); + --width-calendar-date: 2.4rem; + + --opacity-watermark: 0.3; + --color-text-disabled: rgba(0, 0, 0, .3); + } diff --git a/manager-dashboard/app/components/Calendar/CalendarDate/index.tsx b/manager-dashboard/app/components/Calendar/CalendarDate/index.tsx new file mode 100644 index 00000000..2b1fed1a --- /dev/null +++ b/manager-dashboard/app/components/Calendar/CalendarDate/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { _cs } from '@togglecorp/fujs'; + +import RawButton, { Props as RawButtonProps } from '../../RawButton'; +import { ymdToDateString, typedMemo } from '../../../utils/common.tsx'; + +import styles from './styles.css'; + +export interface Props { + className?: string; + year: number; + month: number; + date: number; + currentYear: number; + currentMonth: number; + activeDate?: string; + currentDate: number; + onClick?: (year: number, month: number, date: number) => void; + elementRef?: RawButtonProps['elementRef']; + ghost?: boolean; +} + +function CalendarDate(props: Props) { + const { + className, + year, + month, + date, + currentYear, + currentMonth, + currentDate, + onClick, + elementRef, + activeDate, + ghost, + } = props; + + const handleClick = React.useCallback(() => { + if (onClick) { + onClick(year, month, date); + } + }, [year, month, date, onClick]); + + const dateString = ymdToDateString(year, month, date); + + return ( + + {date} + + + ); +} + +export default typedMemo(CalendarDate); diff --git a/manager-dashboard/app/components/Calendar/CalendarDate/styles.css b/manager-dashboard/app/components/Calendar/CalendarDate/styles.css new file mode 100644 index 00000000..cf87157a --- /dev/null +++ b/manager-dashboard/app/components/Calendar/CalendarDate/styles.css @@ -0,0 +1,25 @@ +.date { + border-radius: 50%; + width: var(--width-calendar-date); + height: var(--width-calendar-date); + + &.today { + color: var(--color-accent); + font-weight: var(--font-weight-bold); + } + + &:hover { + background-color: var(--color-background-hover-light); + } + + &.active { + background-color: var(--color-accent); + color: var(--color-text-on-dark); + pointer-events: none; + } + + &.ghost { + opacity: 0.5; + } +} + diff --git a/manager-dashboard/app/components/Calendar/index.tsx b/manager-dashboard/app/components/Calendar/index.tsx new file mode 100644 index 00000000..d72f9054 --- /dev/null +++ b/manager-dashboard/app/components/Calendar/index.tsx @@ -0,0 +1,292 @@ +import React from 'react'; +import { + _cs, + isNotDefined, + isDefined, +} from '@togglecorp/fujs'; +import { + IoTimeOutline, + IoChevronForward, + IoChevronBack, + IoCalendarOutline, +} from 'react-icons/io5'; + +import Button from '../Button'; +import NumberInput from '../NumberInput'; +import SelectInput from '../SelectInput'; +import useInputState from '../../hooks/useInputState'; +import { typedMemo } from '../../utils/common.tsx'; + +import CalendarDate, { Props as CalendarDateProps } from './CalendarDate'; + +import styles from './styles.css'; + +const weekDayNames = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +interface MonthName { + key: number; + label: string; +} + +const monthNameList: MonthName[] = [ + { key: 0, label: 'January' }, + { key: 1, label: 'February' }, + { key: 2, label: 'March' }, + { key: 3, label: 'April' }, + { key: 4, label: 'May' }, + { key: 5, label: 'June' }, + { key: 6, label: 'July' }, + { key: 7, label: 'August' }, + { key: 8, label: 'September' }, + { key: 9, label: 'October' }, + { key: 10, label: 'November' }, + { key: 11, label: 'December' }, +]; + +function getStartOfWeek(year: number, month: number) { + return new Date(year, month, 1).getDay(); +} + +function getNumDaysInMonth(year: number, month: number) { + // Setting date to 0 will switch the date to last day of previous month + return new Date(year, month + 1, 0).getDate(); +} + +interface RenderDate { + type: 'prevMonth' | 'currentMonth' | 'nextMonth'; + date: number; +} + +function getDates(year: number, month: number) { + const numDays = getNumDaysInMonth(year, month); + const numDayInPrevMonth = getNumDaysInMonth(year, month - 1); + const startOfWeek = getStartOfWeek(year, month); + + const dates: RenderDate[] = []; + + for (let i = 0; i < startOfWeek; i += 1) { + dates.push({ + type: 'prevMonth', + date: numDayInPrevMonth - startOfWeek + i + 1, + }); + } + + for (let i = 0; i < numDays; i += 1) { + dates.push({ + type: 'currentMonth', + date: i + 1, + }); + } + + // 6 rows x 7 cols + const remainingDates = 42 - dates.length; + + for (let i = 0; i < remainingDates; i += 1) { + dates.push({ + type: 'nextMonth', + date: i + 1, + }); + } + + return dates; +} + +const monthKeySelector = (m: MonthName) => m.key; +const monthLabelSelector = (m: MonthName) => m.label; + +type RendererOmissions = 'year' | 'month' | 'date' | 'currentYear' | 'currentMonth' | 'currentDate' | 'onClick' | 'activeDate' | 'ghost'; +export interface Props

{ + className?: string; + dateRenderer?: (props: P) => React.ReactElement; + rendererParams?: (day: number, month: number, year: number) => Omit; + onDateClick?: (day: number, month: number, year: number) => void; + monthSelectionPopupClassName?: string; + initialDate?: string; + activeDate?: string; +} + +function Calendar

(props: Props

) { + const { + className, + dateRenderer: DateRenderer = CalendarDate, + rendererParams, + onDateClick, + monthSelectionPopupClassName, + initialDate, + activeDate, + } = props; + + const today = new Date(); + const current = initialDate ? new Date(initialDate) : today; + const currentYear = current.getFullYear(); + const currentMonth = current.getMonth(); + + const [year, setYear] = useInputState(currentYear); + const [month, setMonth] = useInputState(currentMonth); + + const dates = year ? getDates(year, month) : undefined; + + const handleGotoCurrentButtonClick = React.useCallback(() => { + const date = new Date(); + setYear(date.getFullYear()); + setMonth(date.getMonth()); + }, [setMonth, setYear]); + + const handleNextMonthButtonClick = React.useCallback(() => { + if (isDefined(year)) { + const date = new Date(year, month + 1, 1); + setYear(date.getFullYear()); + setMonth(date.getMonth()); + } + }, [year, month, setMonth, setYear]); + + const handlePreviousMonthButtonClick = React.useCallback(() => { + if (isDefined(year)) { + const date = new Date(year, month - 1, 1); + setYear(date.getFullYear()); + setMonth(date.getMonth()); + } + }, [year, month, setMonth, setYear]); + + const isValidYear = React.useMemo(() => { + if (isNotDefined(year)) { + return false; + } + + if (year < 1900 || year > 9999) { + return false; + } + + return true; + }, [year]); + + return ( +

+
+
+
+ +
+
+ +
+
+
+ {weekDayNames.map((wd) => ( +
+ {wd.substr(0, 2)} +
+ ))} +
+
+ {(isValidYear && isDefined(year) && dates) ? ( +
+ {dates.map((date) => { + let newMonth = month; + if (date.type === 'prevMonth') { + newMonth -= 1; + } else if (date.type === 'nextMonth') { + newMonth += 1; + } + const ymd = new Date(year, newMonth, date.date); + + const defaultProps: Pick = { + onClick: onDateClick, + year: ymd.getFullYear(), + month: ymd.getMonth(), + date: ymd.getDate(), + currentYear: today.getFullYear(), + currentMonth: today.getMonth(), + currentDate: today.getDate(), + activeDate, + ghost: date.type === 'prevMonth' || date.type === 'nextMonth', + }; + + const combinedProps = { + ...(rendererParams ? rendererParams( + date.date, month, year, + ) : undefined), + ...defaultProps, + } as P; + + const children = ( + + ); + + return ( +
+ {children} +
+ ); + })} +
+ ) : ( +
+ + Please select a valid year and month to view the dates +
+ )} +
+ + + +
+
+ ); +} + +export default typedMemo(Calendar); diff --git a/manager-dashboard/app/components/Calendar/styles.css b/manager-dashboard/app/components/Calendar/styles.css new file mode 100644 index 00000000..8d31cba8 --- /dev/null +++ b/manager-dashboard/app/components/Calendar/styles.css @@ -0,0 +1,93 @@ +.calendar { + display: flex; + flex-direction: column; + + .header { + flex-shrink: 0; + + .info { + display: flex; + align-items: flex-end; + justify-content: center; + padding: calc(var(--spacing-medium) - var(--spacing-small)); + + .current-year { + flex-basis: 40%; + padding: var(--spacing-small); + font-size: var(--font-size-large); + } + + .current-month { + flex-basis: 60%; + padding: var(--spacing-small); + } + } + + .week-days { + display: flex; + padding: calc(var(--spacing-medium) - var(--spacing-small)); + + .week-day-name { + display: flex; + align-items: center; + flex-basis: calc(100% / 7); + flex-shrink: 0; + justify-content: center; + padding: var(--spacing-small); + font-weight: var(--font-weight-bold); + } + } + } + + .day-list { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + padding: calc(var(--spacing-medium) - var(--spacing-small)) var(--spacing-medium); + + .day-container { + --width: calc(100% / 7); + display: flex; + align-items: center; + flex-basis: var(--width); + justify-content: center; + width: var(--width); + } + } + + .empty-day-list { + display: flex; + align-items: center; + flex-direction: column; + flex-grow: 1; + justify-content: center; + padding: var(--spacing-large); + text-align: center; + color: var(--color-text); + + .icon { + opacity: var(--opacity-watermark); + margin: var(--spacing-medium); + font-size: var(--font-size-ultra-large); + } + } + + .actions { + display: flex; + flex-shrink: 0; + justify-content: flex-end; + padding: calc(var(--spacing-medium) - var(--spacing-small)); + + >* { + margin: var(--spacing-small) calc(var(--spacing-medium) - var(--spacing-small)); + } + } +} + +.month-selection-popup { + min-width: 10rem; + + .popup-content { + width: 100%!important; + } +} diff --git a/manager-dashboard/app/components/DateRangeInput/index.tsx b/manager-dashboard/app/components/DateRangeInput/index.tsx new file mode 100644 index 00000000..f7e12f53 --- /dev/null +++ b/manager-dashboard/app/components/DateRangeInput/index.tsx @@ -0,0 +1,420 @@ +import React, { useMemo } from 'react'; +import { + _cs, + randomString, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { + IoCalendarOutline, + IoClose, +} from 'react-icons/io5'; + +import useBlurEffect from '../../hooks/useBlurEffect'; +import useBooleanState from '../../hooks/useBooleanState'; +import InputContainer, { Props as InputContainerProps } from '../InputContainer'; +import RawInput from '../RawInput'; +import RawButton from '../RawButton'; +import Button from '../Button'; +import Popup from '../Popup'; +import Calendar, { Props as CalendarProps } from '../Calendar'; +import CalendarDate, { Props as CalendarDateProps } from '../Calendar/CalendarDate'; +import { ymdToDateString, dateStringToDate } from '../../utils/common.tsx'; + +import { + predefinedDateRangeOptions, + PredefinedDateRangeKey, +} from './predefinedDateRange'; + +import styles from './styles.css'; + +// FIXME: this is problematic when on end months +function prevMonth(date: Date) { + const newDate = new Date(date); + newDate.setMonth(newDate.getMonth() - 1); + return newDate; +} +function sameMonth(foo: Date, bar: Date) { + return foo.getFullYear() === bar.getFullYear() && foo.getMonth() === bar.getMonth(); +} + +export interface Value { + startDate: string; + endDate: string; +} + +interface DateRendererProps extends CalendarDateProps { + startDate?: string; + endDate?: string; +} + +function DateRenderer(props: DateRendererProps) { + const { + className: dateClassName, + year, + month, + date, + startDate, + endDate, + ghost, + ...otherProps + } = props; + + const start = startDate ? dateStringToDate(startDate).getTime() : undefined; + const end = endDate ? dateStringToDate(endDate).getTime() : undefined; + const current = new Date(year, month, date).getTime(); + + const inBetween = isDefined(start) && isDefined(end) && current > start && current < end; + + const dateString = ymdToDateString(year, month, date); + + const isEndDate = dateString === endDate; + const isStartDate = dateString === startDate; + + return ( + + ); +} + +type NameType = string | number | undefined; + +type InheritedProps = Omit; +export interface Props extends InheritedProps { + inputElementRef?: React.RefObject; + inputClassName?: string; + value: Value | undefined | null; + name: N; + onChange?: (value: Value | undefined, name: N) => void; + placeholder?: string; +} + +function DateRangeInput(props: Props) { + const { + actions, + actionsContainerClassName, + className, + disabled, + error, + errorContainerClassName, + hint, + hintContainerClassName, + icons, + iconsContainerClassName, + inputSectionClassName, + label, + labelContainerClassName, + readOnly, + inputElementRef, + containerRef: containerRefFromProps, + inputSectionRef: inputSectionRefFromProps, + inputClassName, + onChange, + name, + value, + placeholder, + } = props; + + const [tempDate, setTempDate] = React.useState>({ + startDate: undefined, + endDate: undefined, + }); + const [calendarMonthSelectionPopupClassName] = React.useState(randomString(16)); + const createdContainerRef = React.useRef(null); + const createdInputSectionRef = React.useRef(null); + const popupRef = React.useRef(null); + + const containerRef = containerRefFromProps ?? createdContainerRef; + const inputSectionRef = inputSectionRefFromProps ?? createdInputSectionRef; + const [ + showCalendar, + setShowCalendarTrue, + setShowCalendarFalse,, + toggleShowCalendar, + ] = useBooleanState(false); + + const hideCalendar = React.useCallback(() => { + setTempDate({ + startDate: undefined, + endDate: undefined, + }); + setShowCalendarFalse(); + }, [setShowCalendarFalse]); + + const handlePopupBlur = React.useCallback( + (isClickedWithin: boolean, e: MouseEvent) => { + // Following is to prevent the popup blur when + // month selection is changed in the calendar + const container = document.getElementsByClassName( + calendarMonthSelectionPopupClassName, + )[0]; + const isContainerOrInsideContainer = container + ? container === e.target || container.contains(e.target as HTMLElement) + : false; + if (!isClickedWithin && !isContainerOrInsideContainer) { + hideCalendar(); + } + }, + [hideCalendar, calendarMonthSelectionPopupClassName], + ); + + useBlurEffect( + showCalendar, + handlePopupBlur, + popupRef, + inputSectionRef, + ); + + const dateRendererParams = React.useCallback(() => ({ + startDate: tempDate.startDate ?? value?.startDate, + // we only set end date if user hasn't set the start date + // i.e. to show previously selected end date) + endDate: !tempDate.startDate ? value?.endDate : undefined, + }), [tempDate.startDate, value]); + + const handleCalendarDateClick: CalendarProps['onDateClick'] = React.useCallback( + (year, month, day) => { + setTempDate((prevTempDate) => { + if (isDefined(prevTempDate.startDate)) { + const lastDate = ymdToDateString(year, month, day); + + const prev = dateStringToDate(prevTempDate.startDate).getTime(); + const current = new Date(year, month, day).getTime(); + + const startDate = prev > current ? lastDate : prevTempDate.startDate; + const endDate = prev > current ? prevTempDate.startDate : lastDate; + + return { + startDate, + endDate, + }; + } + + return { + startDate: ymdToDateString(year, month, day), + endDate: undefined, + }; + }); + }, + [], + ); + + React.useEffect(() => { + if (isDefined(tempDate.endDate)) { + if (onChange) { + onChange(tempDate as Value, name); + } + hideCalendar(); + } + }, [tempDate, hideCalendar, onChange, name]); + + const handlePredefinedOptionClick = React.useCallback((optionKey: PredefinedDateRangeKey) => { + if (onChange) { + const option = predefinedDateRangeOptions.find((d) => d.key === optionKey); + + if (option) { + const { + startDate, + endDate, + } = option.getValue(); + + onChange({ + startDate: ymdToDateString( + startDate.getFullYear(), + startDate.getMonth(), + startDate.getDate(), + ), + endDate: ymdToDateString( + endDate.getFullYear(), + endDate.getMonth(), + endDate.getDate(), + ), + }, name); + } + } + + hideCalendar(); + }, [onChange, hideCalendar, name]); + + const handleClearButtonClick = React.useCallback(() => { + if (onChange) { + onChange(undefined, name); + } + }, [onChange, name]); + + const endDate = value?.endDate; + const endDateDate = endDate + ? dateStringToDate(endDate) + : new Date(); + + const startDate = value?.startDate; + let startDateDate = startDate + ? dateStringToDate(startDate) + : new Date(); + + if (sameMonth(endDateDate, startDateDate)) { + startDateDate = prevMonth(startDateDate); + } + + const firstInitialDate = ymdToDateString( + startDateDate.getFullYear(), + startDateDate.getMonth(), + 1, + ); + const secondInitialDate = ymdToDateString( + endDateDate.getFullYear(), + endDateDate.getMonth(), + 1, + ); + + const dateInputLabel = useMemo( + () => { + if ( + isNotDefined(tempDate.startDate) + && isNotDefined(value?.startDate) + && isNotDefined(value?.endDate) + ) { + return undefined; + } + + const startDateString = tempDate.startDate ?? value?.startDate; + const start = isDefined(startDateString) + ? new Date(startDateString).toLocaleDateString() + : '--'; + const endDateString = value?.endDate; + const end = isDefined(endDateString) + ? new Date(endDateString).toLocaleDateString() + : '--'; + + return [ + start, + end, + ].join(' to '); + }, + [value, tempDate], + ); + + return ( + <> + + { actions } + {!readOnly && ( + <> + {value && ( + + )} + + + )} + + )} + actionsContainerClassName={actionsContainerClassName} + className={className} + disabled={disabled} + error={error} + errorContainerClassName={errorContainerClassName} + hint={hint} + hintContainerClassName={hintContainerClassName} + icons={icons} + iconsContainerClassName={iconsContainerClassName} + inputSectionClassName={inputSectionClassName} + inputContainerClassName={styles.inputContainer} + label={label} + labelContainerClassName={labelContainerClassName} + readOnly={readOnly} + input={( + + )} + /> + {!readOnly && showCalendar && ( + +
+ {predefinedDateRangeOptions.map((opt) => ( + + {opt.label} + + ))} +
+ + + +
+ )} + + ); +} + +export default DateRangeInput; diff --git a/manager-dashboard/app/components/DateRangeInput/predefinedDateRange.ts b/manager-dashboard/app/components/DateRangeInput/predefinedDateRange.ts new file mode 100644 index 00000000..4764069c --- /dev/null +++ b/manager-dashboard/app/components/DateRangeInput/predefinedDateRange.ts @@ -0,0 +1,181 @@ +export type PredefinedDateRangeKey = 'today' + | 'yesterday' + | 'thisWeek' + | 'lastSevenDays' + | 'thisMonth' + | 'lastThirtyDays' + | 'lastThreeMonths' + | 'lastSixMonths' + | 'thisYear' + | 'lastYear'; + +export interface PredefinedDateRangeOption { + key: PredefinedDateRangeKey; + label: string; + getValue: () => ({ startDate: Date, endDate: Date }); +} + +export const predefinedDateRangeOptions: PredefinedDateRangeOption[] = [ + { + key: 'today', + label: 'Today', + getValue: () => ({ + startDate: new Date(), + endDate: new Date(), + }), + }, + { + key: 'yesterday', + label: 'Yesterday', + getValue: () => { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 1); + + const endDate = new Date(); + endDate.setDate(endDate.getDate() - 1); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'thisWeek', + label: 'This week', + getValue: () => { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - startDate.getDay()); + + const endDate = new Date(); + // NOTE: this will give us sunday + endDate.setDate(startDate.getDate() + 6); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'lastSevenDays', + label: 'Last 7 days', + getValue: () => { + const endDate = new Date(); + + const startDate = new Date(); + startDate.setDate(endDate.getDate() - 7); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'thisMonth', + label: 'This month', + getValue: () => { + const startDate = new Date(); + startDate.setDate(1); + + const endDate = new Date(); + endDate.setMonth(endDate.getMonth() + 1); + endDate.setDate(0); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'lastThirtyDays', + label: 'Last 30 days', + getValue: () => { + const endDate = new Date(); + + const startDate = new Date(); + startDate.setDate(endDate.getDate() - 30); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'lastThreeMonths', + label: 'Last 3 months', + getValue: () => { + const startDate = new Date(); + startDate.setMonth(startDate.getMonth() - 2); + startDate.setDate(1); + + const endDate = new Date(); + endDate.setMonth(endDate.getMonth() + 1); + endDate.setDate(0); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'lastSixMonths', + label: 'Last 6 months', + getValue: () => { + const startDate = new Date(); + startDate.setMonth(startDate.getMonth() - 5); + startDate.setDate(1); + + const endDate = new Date(); + endDate.setMonth(endDate.getMonth() + 1); + endDate.setDate(0); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'thisYear', + label: 'This year', + getValue: () => { + const startDate = new Date(); + startDate.setMonth(0); + startDate.setDate(1); + + const endDate = new Date(); + endDate.setFullYear(startDate.getFullYear() + 1); + endDate.setMonth(0); + endDate.setDate(0); + + return { + startDate, + endDate, + }; + }, + }, + { + key: 'lastYear', + label: 'Last year', + getValue: () => { + const startDate = new Date(); + startDate.setFullYear(startDate.getFullYear() - 1); + startDate.setMonth(0); + startDate.setDate(1); + + const endDate = new Date(); + endDate.setMonth(0); + endDate.setDate(0); + + return { + startDate, + endDate, + }; + }, + }, +]; diff --git a/manager-dashboard/app/components/DateRangeInput/styles.css b/manager-dashboard/app/components/DateRangeInput/styles.css new file mode 100644 index 00000000..4d4388b7 --- /dev/null +++ b/manager-dashboard/app/components/DateRangeInput/styles.css @@ -0,0 +1,85 @@ +.input-container { + display: flex; + flex-direction: row; + + .input { + --color: var(--color-text); + flex-grow: 1; + min-width: unset; + color: var(--color); + + &.empty { + --color: var(--color-input-placeholder); /* TODO */ + } + + &.errored { + --color: var(--color-danger); + } + } +} + +.calendar-popup { + height: 25rem; + + .popup-content { + display: flex; + max-width: unset!important; + max-height: unset!important; + + .calendar { + --padding: var(--spacing-medium); + width: calc(var(--width-calendar-date) * 7 + 2 * var(--padding)); + height: 100%; + } + + .predefined-options { + display: flex; + flex-direction: column; + justify-content: center; + padding: calc(var(--spacing-medium) - var(--spacing-small)); + + .option { + padding: var(--spacing-small); + width: 100%; + text-align: right; + + &:hover { + background-color: var(--color-background-hover-light); + } + } + } + } +} + +.calendar-date { + &.start-date { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + &:not(.ghost) { + background-color: var(--color-accent); + color: var(--color-text-on-dark); + } + &.ghost { + background-color: var(--color-background-hover-light); + } + } + + &.end-date { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + &:not(.ghost) { + background-color: var(--color-accent); + color: var(--color-text-ligth); + } + &.ghost { + background-color: var(--color-background-hover-light); + } + } + + &.in-between { + border-radius: 0; + background-color: var(--color-background-hover-light); + } +} diff --git a/manager-dashboard/app/utils/common.tsx b/manager-dashboard/app/utils/common.tsx index 1a37b3e8..53338d34 100644 --- a/manager-dashboard/app/utils/common.tsx +++ b/manager-dashboard/app/utils/common.tsx @@ -323,3 +323,17 @@ export const formatProjectTopic = (projectTopic: string) => { return newProjectTopic; }; + +export function ymdToDateString(year: number, month: number, day: number) { + const ys = String(year).padStart(4, '0'); + const ms = String(month + 1).padStart(2, '0'); + const ds = String(day).padStart(2, '0'); + + return `${ys}-${ms}-${ds}`; +} + +export function dateStringToDate(value: string) { + return new Date(`${value}T00:00`); +} + +export const typedMemo: ((c: T) => T) = React.memo; diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index 7bad14c4..6c3c6bef 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -52,6 +52,7 @@ import AnimatedSwipeIcon from '#components/AnimatedSwipeIcon'; import ExpandableContainer from '#components/ExpandableContainer'; import AlertBanner from '#components/AlertBanner'; import Checkbox from '#components/Checkbox'; +import DateRangeInput from '#components/DateRangeInput'; import { valueSelector, labelSelector, @@ -317,6 +318,9 @@ function NewProject(props: Props) { valuesToCopy.geometry = res.geometry; } + valuesToCopy.startTimestamp = valuesToCopy.dateRange?.startDate || null; + valuesToCopy.endTimestamp = valuesToCopy.dateRange?.endDate || null; + const storage = getStorage(); const timestamp = (new Date()).getTime(); const uploadedImageRef = storageRef(storage, `projectImages/${timestamp}-project-image-${projectImage.name}`); @@ -711,17 +715,18 @@ function NewProject(props: Props) { )} {value.projectType === PROJECT_TYPE_STREET && ( - /* TODO: Add street project inputs for - startTimestamp, - endTimeStamp, - isPano, - organizationId, - samplingThreshold - */ -
time range input
+ Date: Tue, 17 Dec 2024 13:50:22 +0100 Subject: [PATCH 10/11] feat(manager-dashboard): add creator id input to mapillary image filters --- manager-dashboard/app/views/NewProject/index.tsx | 15 ++++++++++++--- manager-dashboard/app/views/NewProject/utils.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx index 6c3c6bef..9d4acf35 100644 --- a/manager-dashboard/app/views/NewProject/index.tsx +++ b/manager-dashboard/app/views/NewProject/index.tsx @@ -318,8 +318,8 @@ function NewProject(props: Props) { valuesToCopy.geometry = res.geometry; } - valuesToCopy.startTimestamp = valuesToCopy.dateRange?.startDate || null; - valuesToCopy.endTimestamp = valuesToCopy.dateRange?.endDate || null; + valuesToCopy.startTimestamp = valuesToCopy.dateRange?.startDate ?? null; + valuesToCopy.endTimestamp = valuesToCopy.dateRange?.endDate ?? null; const storage = getStorage(); const timestamp = (new Date()).getTime(); @@ -720,13 +720,22 @@ function NewProject(props: Props) { > + Date: Tue, 17 Dec 2024 14:13:16 +0100 Subject: [PATCH 11/11] feat(manager-dashboard): adjust value interface --- manager-dashboard/app/components/DateRangeInput/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manager-dashboard/app/components/DateRangeInput/index.tsx b/manager-dashboard/app/components/DateRangeInput/index.tsx index f7e12f53..6442fc83 100644 --- a/manager-dashboard/app/components/DateRangeInput/index.tsx +++ b/manager-dashboard/app/components/DateRangeInput/index.tsx @@ -39,8 +39,8 @@ function sameMonth(foo: Date, bar: Date) { } export interface Value { - startDate: string; - endDate: string; + startDate?: string; + endDate?: string; } interface DateRendererProps extends CalendarDateProps {