From cbe7ac15514fe9632b2d20044fcba5503395e096 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Wed, 13 Sep 2023 16:19:27 +0200 Subject: [PATCH 1/9] feat: Added components DateTimeRangePicker and TimeInput --- .../components/ExampleDateTimeRangePicker.tsx | 11 + apps/docs/components/ExampleTimeInput.tsx | 14 + apps/docs/package.json | 1 + .../components/date-time-range-picker.mdx | 33 +++ .../pages/react-ui/components/time-input.mdx | 33 +++ .../DateTimeRangePicker.tsx | 258 ++++++++++++++++++ .../react-ui/DateTimeRangePicker/index.tsx | 1 + packages/react-ui/TimeInput/TimeInput.tsx | 91 ++++++ packages/react-ui/TimeInput/index.tsx | 1 + .../Added DateTimeRangePicker component | 0 .../changes/Added TimeInput component | 0 packages/react-ui/index.tsx | 2 + packages/react-ui/package.json | 6 +- packages/react-ui/temp/react-ui.api.md | 24 ++ pnpm-lock.yaml | 187 ++++++++++++- 15 files changed, 660 insertions(+), 2 deletions(-) create mode 100644 apps/docs/components/ExampleDateTimeRangePicker.tsx create mode 100644 apps/docs/components/ExampleTimeInput.tsx create mode 100644 apps/docs/pages/react-ui/components/date-time-range-picker.mdx create mode 100644 apps/docs/pages/react-ui/components/time-input.mdx create mode 100644 packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx create mode 100644 packages/react-ui/DateTimeRangePicker/index.tsx create mode 100644 packages/react-ui/TimeInput/TimeInput.tsx create mode 100644 packages/react-ui/TimeInput/index.tsx create mode 100644 packages/react-ui/changes/Added DateTimeRangePicker component create mode 100644 packages/react-ui/changes/Added TimeInput component diff --git a/apps/docs/components/ExampleDateTimeRangePicker.tsx b/apps/docs/components/ExampleDateTimeRangePicker.tsx new file mode 100644 index 00000000..f8cfc4ed --- /dev/null +++ b/apps/docs/components/ExampleDateTimeRangePicker.tsx @@ -0,0 +1,11 @@ +import { DateTimeRangePicker } from '@enterwell/react-ui'; +import moment from 'moment'; + +export function ExampleDateTimeRangePicker() { + return ( + {}} /> + ) +} \ No newline at end of file diff --git a/apps/docs/components/ExampleTimeInput.tsx b/apps/docs/components/ExampleTimeInput.tsx new file mode 100644 index 00000000..3f2d88eb --- /dev/null +++ b/apps/docs/components/ExampleTimeInput.tsx @@ -0,0 +1,14 @@ +import { TimeInput } from '@enterwell/react-ui'; +import { useState } from 'react'; + +export function ExampleTimeInput() { + const [value, setValue] = useState('12:00'); + + return ( + // @highlight-start + + // @highlight-end + ) +} \ No newline at end of file diff --git a/apps/docs/package.json b/apps/docs/package.json index 3d874aec..0d36190d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -30,6 +30,7 @@ "@mui/material": "^5.14.8", "classix": "^2.1.34", "comment-parser": "^1.4.0", + "moment": "^2.29.4", "next": "13.4.19", "nextra": "latest", "nextra-theme-docs": "latest", diff --git a/apps/docs/pages/react-ui/components/date-time-range-picker.mdx b/apps/docs/pages/react-ui/components/date-time-range-picker.mdx new file mode 100644 index 00000000..9dcd5307 --- /dev/null +++ b/apps/docs/pages/react-ui/components/date-time-range-picker.mdx @@ -0,0 +1,33 @@ +--- +title: DateTimeRangePicker +--- + +import { DateTimeRangePicker } from '@enterwell/react-ui'; +import { ComponentWithSource } from '../../../components/docs/ComponentWithSource.tsx'; +import { ExampleDateTimeRangePicker } from '../../../components/ExampleDateTimeRangePicker.tsx'; +import { ComponentDescription, ComponentParameters, ComponentSource } from '../../../components/docs/ComponentDocs'; + +# DropdownButton + +## Description + + + +### Parameters + + + +## Example + + + +## Inspect + +
+ Source code + +
\ No newline at end of file diff --git a/apps/docs/pages/react-ui/components/time-input.mdx b/apps/docs/pages/react-ui/components/time-input.mdx new file mode 100644 index 00000000..2cff581a --- /dev/null +++ b/apps/docs/pages/react-ui/components/time-input.mdx @@ -0,0 +1,33 @@ +--- +title: TimeInput +--- + +import { TimeInput } from '@enterwell/react-ui'; +import { ComponentWithSource } from '../../../components/docs/ComponentWithSource.tsx'; +import { ExampleTimeInput } from '../../../components/ExampleTimeInput.tsx'; +import { ComponentDescription, ComponentParameters, ComponentSource } from '../../../components/docs/ComponentDocs'; + +# DropdownButton + +## Description + + + +### Parameters + + + +## Example + + + +## Inspect + +
+ Source code + +
\ No newline at end of file diff --git a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx new file mode 100644 index 00000000..938e99a3 --- /dev/null +++ b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -0,0 +1,258 @@ +import { + Box, Button, Grid, Popover, TextField, useTheme, useMediaQuery +} from '@mui/material'; +import { ComponentProps, MouseEvent, useState } from 'react'; +import { StaticDateRangePicker, LocalizationProvider } from '@mui/x-date-pickers-pro'; +import { AdapterDateFns } from '@mui/x-date-pickers-pro/AdapterDateFns'; +import moment, { type Moment } from 'moment'; +import hrLocale from 'date-fns/locale/hr'; +import { TimeInput } from '../TimeInput'; + +/** + * The date time range picker props. + * @public + */ +export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title"> & { + start: Moment; + end: Moment; + onChange: (startDate: Moment, endDate: Moment) => void; + hideTime?: boolean; + dense?: boolean; +}; + +/** +* The date time range picker. +* +* @param props - The props; +* @returns Returns date time range picker component. +* @public +*/ +export function DateTimeRangePicker({ + start: propStartDate, + end: propEndDate, + hideTime, + onChange, + ...rest +}: DateTimeRangePickerProps) { + const [anchorEl, setAnchorEl] = useState(null); + const [startTime, setStartTime] = useState(moment(propStartDate).format('HH:mm')); + const [endTime, setEndTime] = useState(propEndDate.format('HH:mm')); + + // Use js dates because we use date-fns adapter + const defaultStartDate = moment(propStartDate).startOf('day').toDate(); + const defaultEndDate = moment(propEndDate).startOf('day').toDate(); + + const [dateValue, setDateValue] = useState<[Date | null, Date | null]>([defaultStartDate, defaultEndDate]); + const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up('sm')); + + /** + * Handles the picker input click. + * This will open picker popover. + * + * @param event - The click event + */ + const handleOpen = (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + /** + * Handles the picker close. + * This will close the picker. + */ + const handleClose = () => { + setAnchorEl(null); + }; + + /** + * Combines the date and time into. + * + * @returns First element is start date and time, seconds is end date and time. + */ + const combineDateAndTime = () => { + let startDuration = moment.duration(startTime); + const endDuration = moment.duration(endTime); + + // Check if start is after end - set equal + if (moment(dateValue[0]).startOf('day').isSame(moment(dateValue[1]).startOf('day')) + && startDuration.asMilliseconds() > endDuration.asMilliseconds()) { + startDuration = endDuration; + } + + const startDateTime = moment(dateValue[0]).startOf('day'); + const endDateTime = moment(dateValue[1]).startOf('day'); + startDateTime.add(startDuration.asMilliseconds(), 'ms'); + endDateTime.add(endDuration.asMilliseconds(), 'ms'); + return [startDateTime, endDateTime]; + }; + + /** + * Handles the picker accept option. + */ + const handleAccept = () => { + const combined = combineDateAndTime(); + onChange(combined[0], combined[1]); + handleClose(); + }; + + /** + * Handle the preselect option click. + * This will set start and end date to preselected values. + * + * @param startDate The start date and time. + * @param endDate The end date and time. + */ + const handlePreselect = (startDate: Date, endDate: Date) => { + setDateValue([startDate, endDate]); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + const preselectedOptions = [ + { name: 'Danas', startDate: moment().startOf('day'), endDate: moment().endOf('day') }, + { name: 'Jučer', startDate: moment().subtract(1, 'days').startOf('day'), endDate: moment().subtract(1, 'days').endOf('day') }, + { name: 'Proteklih 7 dana', startDate: moment().subtract(7, 'days').startOf('day'), endDate: moment().endOf('day') }, + { name: 'Proteklih 30 dana', startDate: moment().subtract(30, 'days').startOf('day'), endDate: moment().endOf('day') }, + { name: 'Ovaj mjesec', startDate: moment().startOf('month'), endDate: moment().endOf('day') }, + { name: 'Prošli mjesec', startDate: moment().subtract(1, 'months').startOf('month'), endDate: moment().subtract(1, 'months').endOf('month') } + ]; + + const startValueStringFormat = hideTime || moment(propStartDate).startOf('day').isSame(propStartDate, 'minute') + ? 'DD.MM.YYYY.' + : 'DD.MM.YYYY. HH:mm'; + const endValueStringFormat = hideTime || moment(propEndDate).endOf('day').isSame(propEndDate, 'minute') + ? 'DD.MM.YYYY.' + : 'DD.MM.YYYY. HH:mm'; + const valueString = `${moment(propStartDate).format(startValueStringFormat)} do ${moment(propEndDate).format(endValueStringFormat)}`; + + return ( + <> + + + + + + + + + + + + + + + + + {preselectedOptions.map((opt) => ( + + + + ))} + + + + + + {!hideTime && ( + <> + + + + + setDateValue([e.target.value as unknown as Date, dateValue[1]])} + fullWidth + size="small" + label="Od" + InputLabelProps={{ + shrink: true, + }} + /> + + + setDateValue([dateValue[0], e.target.value as unknown as Date])} + fullWidth + size="small" + label="Do" + InputLabelProps={{ + shrink: true, + }} + /> + + + + + + + + + + + + + + + + + + )} + + + ( + <> + + do + + + )} + /> + + + + + + + + ); +}; diff --git a/packages/react-ui/DateTimeRangePicker/index.tsx b/packages/react-ui/DateTimeRangePicker/index.tsx new file mode 100644 index 00000000..0b7e537e --- /dev/null +++ b/packages/react-ui/DateTimeRangePicker/index.tsx @@ -0,0 +1 @@ +export * from "./DateTimeRangePicker"; diff --git a/packages/react-ui/TimeInput/TimeInput.tsx b/packages/react-ui/TimeInput/TimeInput.tsx new file mode 100644 index 00000000..2d1254df --- /dev/null +++ b/packages/react-ui/TimeInput/TimeInput.tsx @@ -0,0 +1,91 @@ +import { + TextField +} from '@mui/material'; +import { type ChangeEvent, type ComponentProps, type FocusEvent } from 'react'; + +function processTime(time: unknown, useSeconds: boolean) { + const timeMatch = typeof time === 'string' ? time?.match(/\d/g) : null; + if (timeMatch == null) { + return '00:00' + (useSeconds ? ':00' : ''); + } + + const timeString = timeMatch.length % 2 === 0 + ? (time as string).toString() + : '0' + (time as string).toString(); + const startTimeMatch = timeString.match(/(([0-1][0-9]?)|(2[0-3]))?:?(\d\d?)?:?(\d\d?)?/); + if (!startTimeMatch) { + return '00:00' + (useSeconds ? ':00' : ''); + } + + let h = Number.parseInt(startTimeMatch[1], 10) || 0; + let m = Number.parseInt(startTimeMatch[4], 10) || 0; + let s = Number.parseInt(startTimeMatch[5], 10) || 0; + m += Math.floor(s / 60); + s %= 60; + h += Math.floor(m / 60); + m %= 60; + h %= 24; + return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}${useSeconds && s > 0 ? ':' + s.toString().padStart(2, '0') : ''}`; +} + +/** + * The time input props. + * @public + */ +export type TimeInputProps = ComponentProps & { + useSeconds?: boolean; + onTimeChange?: (time: string) => void; +} + +/** +* The time input component. +* +* @param props - The props; +* @returns The time input component. +* @public +*/ +export function TimeInput({ + value, + defaultValue, + useSeconds, + onChange, + onBlur, + onTimeChange, + ...rest +}: TimeInputProps) { + /** + * Handles the input blur. + * This will call onTimeChange callback. + */ + const handleBlur = (event: FocusEvent) => { + if (onBlur) { + onBlur(event); + } + if (onTimeChange) { + onTimeChange(processTime(value ?? defaultValue, useSeconds ?? false)); + } + }; + + /** + * Handles the input change. + * This will call onTimeChange callback. + */ + function handleChange(event: ChangeEvent) { + if (onChange) { + onChange(event); + } + if (onTimeChange) { + onTimeChange(event.target.value); + } + } + + return ( + + ); +} diff --git a/packages/react-ui/TimeInput/index.tsx b/packages/react-ui/TimeInput/index.tsx new file mode 100644 index 00000000..2d91c139 --- /dev/null +++ b/packages/react-ui/TimeInput/index.tsx @@ -0,0 +1 @@ +export * from "./TimeInput"; diff --git a/packages/react-ui/changes/Added DateTimeRangePicker component b/packages/react-ui/changes/Added DateTimeRangePicker component new file mode 100644 index 00000000..e69de29b diff --git a/packages/react-ui/changes/Added TimeInput component b/packages/react-ui/changes/Added TimeInput component new file mode 100644 index 00000000..e69de29b diff --git a/packages/react-ui/index.tsx b/packages/react-ui/index.tsx index 379329af..d9518d38 100644 --- a/packages/react-ui/index.tsx +++ b/packages/react-ui/index.tsx @@ -1,5 +1,7 @@ 'use client'; // component exports +export * from "./TimeInput"; +export * from "./DateTimeRangePicker"; export * from "./DropdownButton"; export * from "./PageDrawer"; \ No newline at end of file diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index 673e9e6b..b2aeb792 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -24,17 +24,20 @@ "lint": "eslint ." }, "devDependencies": { - "@enterwell/react-hooks": "workspace:*", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@enterwell/react-hooks": "workspace:*", "@microsoft/api-extractor": "^7.36.4", "@mui/icons-material": "^5.14.8", "@mui/material": "^5.14.8", "@mui/system": "^5.14.8", + "@mui/x-date-pickers-pro": "5.0.20", "@types/node": "^20.5.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "date-fns": "^2.30.0", "eslint-config-custom": "workspace:*", + "moment": "^2.29.4", "react": "^18.2.0", "rimraf": "^5.0.1", "tsconfig": "workspace:*", @@ -46,6 +49,7 @@ "@emotion/styled": "^11.11.0", "@mui/material": "^5.14.8", "@mui/system": "^5.14.8", + "@mui/x-date-pickers-pro": "5.0.20", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/packages/react-ui/temp/react-ui.api.md b/packages/react-ui/temp/react-ui.api.md index 176a063e..a86f1114 100644 --- a/packages/react-ui/temp/react-ui.api.md +++ b/packages/react-ui/temp/react-ui.api.md @@ -5,9 +5,24 @@ ```ts import { ButtonProps } from '@mui/material'; +import { ComponentProps } from 'react'; import { HTMLAttributes } from 'react'; +import { Moment } from 'moment'; import * as react_jsx_runtime from 'react/jsx-runtime'; import { ReactElement } from 'react'; +import { TextField } from '@mui/material'; + +// @public +export function DateTimeRangePicker({ start: propStartDate, end: propEndDate, hideTime, onChange, dense }: DateTimeRangePickerProps): react_jsx_runtime.JSX.Element; + +// @public +export type DateTimeRangePickerProps = { + start: Moment; + end: Moment; + onChange: (startDate: Moment, endDate: Moment) => void; + hideTime?: boolean; + dense?: boolean; +}; // @public export function DropdownButton({ options, onClick, icon, ...rest }: DropdownButtonProps): react_jsx_runtime.JSX.Element; @@ -34,6 +49,15 @@ export type PageDrawerProps = HTMLAttributes & { onChange?: () => void; }; +// @public +export function TimeInput({ value, defaultValue, useSeconds, onBlur, onTimeChange, ...rest }: TimeInputProps): react_jsx_runtime.JSX.Element; + +// @public +export type TimeInputProps = ComponentProps & { + useSeconds?: boolean; + onTimeChange?: (time: string) => void; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60f9aef0..b4a0b993 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: comment-parser: specifier: ^1.4.0 version: 1.4.0 + moment: + specifier: ^2.29.4 + version: 2.29.4 next: specifier: 13.4.19 version: 13.4.19(react-dom@18.2.0)(react@18.2.0) @@ -176,6 +179,9 @@ importers: '@mui/system': specifier: ^5.14.8 version: 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react@18.2.0) + '@mui/x-date-pickers-pro': + specifier: 5.0.20 + version: 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: ^20.5.2 version: 20.5.2 @@ -185,9 +191,15 @@ importers: '@types/react-dom': specifier: ^18.2.0 version: 18.2.7 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 eslint-config-custom: specifier: workspace:* version: link:../eslint-config-custom + moment: + specifier: ^2.29.4 + version: 2.29.4 react: specifier: ^18.2.0 version: 18.2.0 @@ -454,6 +466,56 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@date-io/core@2.17.0: + resolution: {integrity: sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==} + dev: true + + /@date-io/date-fns@2.17.0(date-fns@2.30.0): + resolution: {integrity: sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==} + peerDependencies: + date-fns: ^2.0.0 + peerDependenciesMeta: + date-fns: + optional: true + dependencies: + '@date-io/core': 2.17.0 + date-fns: 2.30.0 + dev: true + + /@date-io/dayjs@2.17.0: + resolution: {integrity: sha512-Iq1wjY5XzBh0lheFA0it6Dsyv94e8mTiNR8vuTai+KopxDkreL3YjwTmZHxkgB7/vd0RMIACStzVgWvPATnDCA==} + peerDependencies: + dayjs: ^1.8.17 + peerDependenciesMeta: + dayjs: + optional: true + dependencies: + '@date-io/core': 2.17.0 + dev: true + + /@date-io/luxon@2.17.0: + resolution: {integrity: sha512-l712Vdm/uTddD2XWt9TlQloZUiTiRQtY5TCOG45MQ/8u0tu8M17BD6QYHar/3OrnkGybALAMPzCy1r5D7+0HBg==} + peerDependencies: + luxon: ^1.21.3 || ^2.x || ^3.x + peerDependenciesMeta: + luxon: + optional: true + dependencies: + '@date-io/core': 2.17.0 + dev: true + + /@date-io/moment@2.17.0(moment@2.29.4): + resolution: {integrity: sha512-e4nb4CDZU4k0WRVhz1Wvl7d+hFsedObSauDHKtZwU9kt7gdYEAzKgnrSCTHsEaXrDumdrkCYTeZ0Tmyk7uV4tw==} + peerDependencies: + moment: ^2.24.0 + peerDependenciesMeta: + moment: + optional: true + dependencies: + '@date-io/core': 2.17.0 + moment: 2.29.4 + dev: true + /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: @@ -1171,6 +1233,112 @@ packages: react: 18.2.0 react-is: 18.2.0 + /@mui/x-date-pickers-pro@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-syNnokcG4lvj9SIcnfji6eVvpSpuHs+7jqLsJYDBTogxK53ctSXQVMb5GQ16ioTzEQSqZojhngHSj+/s11Ao1Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.4.1 + '@mui/system': ^5.4.1 + date-fns: ^2.25.0 + dayjs: ^1.10.7 + luxon: ^1.28.0 || ^2.0.0 || ^3.0.0 + moment: ^2.29.1 + react: ^17.0.2 || ^18.0.0 + react-dom: ^17.0.2 || ^18.0.0 + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + dependencies: + '@babel/runtime': 7.22.15 + '@date-io/date-fns': 2.17.0(date-fns@2.30.0) + '@date-io/dayjs': 2.17.0 + '@date-io/luxon': 2.17.0 + '@date-io/moment': 2.17.0(moment@2.29.4) + '@mui/material': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react@18.2.0) + '@mui/utils': 5.14.8(react@18.2.0) + '@mui/x-date-pickers': 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-license-pro': 5.17.12(react@18.2.0) + clsx: 1.2.1 + date-fns: 2.30.0 + moment: 2.29.4 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + rifm: 0.12.1(react@18.2.0) + transitivePeerDependencies: + - '@emotion/react' + - '@emotion/styled' + dev: true + + /@mui/x-date-pickers@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ERukSeHIoNLbI1C2XRhF9wRhqfsr+Q4B1SAw2ZlU7CWgcG8UBOxgqRKDEOVAIoSWL+DWT6GRuQjOKvj6UXZceA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.4.1 + '@mui/system': ^5.4.1 + date-fns: ^2.25.0 + dayjs: ^1.10.7 + luxon: ^1.28.0 || ^2.0.0 || ^3.0.0 + moment: ^2.29.1 + react: ^17.0.2 || ^18.0.0 + react-dom: ^17.0.2 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + dependencies: + '@babel/runtime': 7.22.15 + '@date-io/core': 2.17.0 + '@date-io/date-fns': 2.17.0(date-fns@2.30.0) + '@date-io/dayjs': 2.17.0 + '@date-io/luxon': 2.17.0 + '@date-io/moment': 2.17.0(moment@2.29.4) + '@emotion/react': 11.11.1(@types/react@18.2.21)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.21)(react@18.2.0) + '@mui/material': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react@18.2.0) + '@mui/utils': 5.14.8(react@18.2.0) + '@types/react-transition-group': 4.4.6 + clsx: 1.2.1 + date-fns: 2.30.0 + moment: 2.29.4 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + rifm: 0.12.1(react@18.2.0) + dev: true + + /@mui/x-license-pro@5.17.12(react@18.2.0): + resolution: {integrity: sha512-UzFaE+9A30kfguCuME0D5zqsItqbHZ3xZwmyrJr8MvZOEoqiJWF4NT4Pvlg2nqaPYt/h81j7sjVFa3KiwT0vbg==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.2 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.15 + '@mui/utils': 5.14.8(react@18.2.0) + react: 18.2.0 + dev: true + /@napi-rs/simple-git-android-arm-eabi@0.1.9: resolution: {integrity: sha512-9D4JnfePMpgL4pg9aMUX7/TIWEUQ+Tgx8n3Pf8TNCMGjUbImJyYsDSLJzbcv9wH7srgn4GRjSizXFJHAPjzEug==} engines: {node: '>= 10'} @@ -2511,7 +2679,6 @@ packages: /clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - dev: false /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} @@ -2962,6 +3129,13 @@ packages: engines: {node: '>= 14'} dev: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.15 + dev: true + /dayjs@1.11.9: resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} dev: false @@ -5969,6 +6143,9 @@ packages: minimist: 1.2.8 dev: true + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -7002,6 +7179,14 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rifm@0.12.1(react@18.2.0): + resolution: {integrity: sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==} + peerDependencies: + react: '>=16.8' + dependencies: + react: 18.2.0 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true From c44e6a6b593f03a2996c8c27d2aa7c7e1ec54752 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Wed, 13 Sep 2023 16:19:52 +0200 Subject: [PATCH 2/9] Update react-ui.api.md --- packages/react-ui/temp/react-ui.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/temp/react-ui.api.md b/packages/react-ui/temp/react-ui.api.md index a86f1114..fac579e4 100644 --- a/packages/react-ui/temp/react-ui.api.md +++ b/packages/react-ui/temp/react-ui.api.md @@ -13,10 +13,10 @@ import { ReactElement } from 'react'; import { TextField } from '@mui/material'; // @public -export function DateTimeRangePicker({ start: propStartDate, end: propEndDate, hideTime, onChange, dense }: DateTimeRangePickerProps): react_jsx_runtime.JSX.Element; +export function DateTimeRangePicker({ start: propStartDate, end: propEndDate, hideTime, onChange, ...rest }: DateTimeRangePickerProps): react_jsx_runtime.JSX.Element; // @public -export type DateTimeRangePickerProps = { +export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title"> & { start: Moment; end: Moment; onChange: (startDate: Moment, endDate: Moment) => void; @@ -50,7 +50,7 @@ export type PageDrawerProps = HTMLAttributes & { }; // @public -export function TimeInput({ value, defaultValue, useSeconds, onBlur, onTimeChange, ...rest }: TimeInputProps): react_jsx_runtime.JSX.Element; +export function TimeInput({ value, defaultValue, useSeconds, onChange, onBlur, onTimeChange, ...rest }: TimeInputProps): react_jsx_runtime.JSX.Element; // @public export type TimeInputProps = ComponentProps & { From 2d2ab793b4bf12cc79854ba2d9cd091eca04714a Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 13:49:23 +0200 Subject: [PATCH 3/9] fix: Removing moment --- .../components/ExampleDateTimeRangePicker.tsx | 11 ++- .../components/date-time-range-picker.mdx | 2 +- .../pages/react-ui/components/time-input.mdx | 2 +- .../DateTimeRangePicker.tsx | 90 +++++++++++-------- 4 files changed, 61 insertions(+), 44 deletions(-) diff --git a/apps/docs/components/ExampleDateTimeRangePicker.tsx b/apps/docs/components/ExampleDateTimeRangePicker.tsx index f8cfc4ed..fc269df4 100644 --- a/apps/docs/components/ExampleDateTimeRangePicker.tsx +++ b/apps/docs/components/ExampleDateTimeRangePicker.tsx @@ -1,11 +1,14 @@ import { DateTimeRangePicker } from '@enterwell/react-ui'; -import moment from 'moment'; +import { useState } from 'react'; export function ExampleDateTimeRangePicker() { + const [start, setStart] = useState(new Date(new Date().getTime() - 1000 * 60 * 60 * 24)); + const [end, setEnd] = useState(new Date()); + return ( {}} /> + start={start} + end={end} + onChange={(start, end) => { setStart(start); setEnd(end); }} /> ) } \ No newline at end of file diff --git a/apps/docs/pages/react-ui/components/date-time-range-picker.mdx b/apps/docs/pages/react-ui/components/date-time-range-picker.mdx index 9dcd5307..13cef101 100644 --- a/apps/docs/pages/react-ui/components/date-time-range-picker.mdx +++ b/apps/docs/pages/react-ui/components/date-time-range-picker.mdx @@ -7,7 +7,7 @@ import { ComponentWithSource } from '../../../components/docs/ComponentWithSourc import { ExampleDateTimeRangePicker } from '../../../components/ExampleDateTimeRangePicker.tsx'; import { ComponentDescription, ComponentParameters, ComponentSource } from '../../../components/docs/ComponentDocs'; -# DropdownButton +# DateTimeRangePicker ## Description diff --git a/apps/docs/pages/react-ui/components/time-input.mdx b/apps/docs/pages/react-ui/components/time-input.mdx index 2cff581a..5f4d2237 100644 --- a/apps/docs/pages/react-ui/components/time-input.mdx +++ b/apps/docs/pages/react-ui/components/time-input.mdx @@ -7,7 +7,7 @@ import { ComponentWithSource } from '../../../components/docs/ComponentWithSourc import { ExampleTimeInput } from '../../../components/ExampleTimeInput.tsx'; import { ComponentDescription, ComponentParameters, ComponentSource } from '../../../components/docs/ComponentDocs'; -# DropdownButton +# TimeInput ## Description diff --git a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx index 938e99a3..f92203bb 100644 --- a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -4,18 +4,18 @@ import { import { ComponentProps, MouseEvent, useState } from 'react'; import { StaticDateRangePicker, LocalizationProvider } from '@mui/x-date-pickers-pro'; import { AdapterDateFns } from '@mui/x-date-pickers-pro/AdapterDateFns'; -import moment, { type Moment } from 'moment'; import hrLocale from 'date-fns/locale/hr'; +import { parse, format, isSameDay, isSameMinute, startOfDay, endOfDay, startOfYesterday, endOfYesterday, sub, startOfMonth, endOfMonth } from 'date-fns'; import { TimeInput } from '../TimeInput'; /** * The date time range picker props. * @public */ -export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title"> & { - start: Moment; - end: Moment; - onChange: (startDate: Moment, endDate: Moment) => void; +export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title" | "onChange"> & { + start: Date; + end: Date; + onChange: (startDate: Date, endDate: Date) => void; hideTime?: boolean; dense?: boolean; }; @@ -35,12 +35,12 @@ export function DateTimeRangePicker({ ...rest }: DateTimeRangePickerProps) { const [anchorEl, setAnchorEl] = useState(null); - const [startTime, setStartTime] = useState(moment(propStartDate).format('HH:mm')); - const [endTime, setEndTime] = useState(propEndDate.format('HH:mm')); + const [startTime, setStartTime] = useState(format(propStartDate, 'HH:mm')); + const [endTime, setEndTime] = useState(format(propEndDate, 'HH:mm')); // Use js dates because we use date-fns adapter - const defaultStartDate = moment(propStartDate).startOf('day').toDate(); - const defaultEndDate = moment(propEndDate).startOf('day').toDate(); + const defaultStartDate = startOfDay(propStartDate); + const defaultEndDate = startOfDay(propEndDate); const [dateValue, setDateValue] = useState<[Date | null, Date | null]>([defaultStartDate, defaultEndDate]); const theme = useTheme(); @@ -58,31 +58,43 @@ export function DateTimeRangePicker({ /** * Handles the picker close. - * This will close the picker. + * This will close the picker and reset selection. */ const handleClose = () => { setAnchorEl(null); + setDateValue([defaultStartDate, defaultEndDate]); + setStartTime(format(propStartDate, 'HH:mm')); + setEndTime(format(propEndDate, 'HH:mm')); }; /** - * Combines the date and time into. + * Combines the date and time into moment objects. * * @returns First element is start date and time, seconds is end date and time. */ const combineDateAndTime = () => { - let startDuration = moment.duration(startTime); - const endDuration = moment.duration(endTime); + if (dateValue[0] == null || dateValue[1] == null) + return []; + + let startDuration = parse(startTime, "HH:mm", 0); + const endDuration = parse(endTime, "HH:mm", 0); // Check if start is after end - set equal - if (moment(dateValue[0]).startOf('day').isSame(moment(dateValue[1]).startOf('day')) - && startDuration.asMilliseconds() > endDuration.asMilliseconds()) { + if (dateValue[0] != null + && dateValue[1] != null + && isSameDay(dateValue[0], dateValue[1]) + && startDuration.getTime() > endDuration.getTime()) { startDuration = endDuration; } - const startDateTime = moment(dateValue[0]).startOf('day'); - const endDateTime = moment(dateValue[1]).startOf('day'); - startDateTime.add(startDuration.asMilliseconds(), 'ms'); - endDateTime.add(endDuration.asMilliseconds(), 'ms'); + const startDateTime = dateValue[0] ? startOfDay(dateValue[0]) : undefined; + if (startDateTime != null) { + startDateTime.setMilliseconds(startDateTime.getMilliseconds() + startDuration.getTime()); + } + const endDateTime = dateValue[1] ? startOfDay(dateValue[1]) : undefined; + if (endDateTime != null) { + endDateTime.setMilliseconds(endDateTime.getMilliseconds() + endDuration.getTime()); + } return [startDateTime, endDateTime]; }; @@ -91,8 +103,10 @@ export function DateTimeRangePicker({ */ const handleAccept = () => { const combined = combineDateAndTime(); - onChange(combined[0], combined[1]); - handleClose(); + if (combined[0] != null && combined[1] != null) { + onChange(combined[0], combined[1]); + setAnchorEl(null); + } }; /** @@ -109,22 +123,22 @@ export function DateTimeRangePicker({ const open = Boolean(anchorEl); const id = open ? 'simple-popover' : undefined; - const preselectedOptions = [ - { name: 'Danas', startDate: moment().startOf('day'), endDate: moment().endOf('day') }, - { name: 'Jučer', startDate: moment().subtract(1, 'days').startOf('day'), endDate: moment().subtract(1, 'days').endOf('day') }, - { name: 'Proteklih 7 dana', startDate: moment().subtract(7, 'days').startOf('day'), endDate: moment().endOf('day') }, - { name: 'Proteklih 30 dana', startDate: moment().subtract(30, 'days').startOf('day'), endDate: moment().endOf('day') }, - { name: 'Ovaj mjesec', startDate: moment().startOf('month'), endDate: moment().endOf('day') }, - { name: 'Prošli mjesec', startDate: moment().subtract(1, 'months').startOf('month'), endDate: moment().subtract(1, 'months').endOf('month') } + const preselectedOptions: { name: string, startDate: Date, endDate: Date }[] = [ + { name: 'Danas', startDate: startOfDay(new Date()), endDate: endOfDay(new Date()) }, + { name: 'Jučer', startDate: startOfYesterday(), endDate: endOfYesterday() }, + { name: 'Proteklih 7 dana', startDate: sub(startOfDay(new Date()), { days: 7 }), endDate: endOfDay(new Date()) }, + { name: 'Proteklih 30 dana', startDate: sub(startOfDay(new Date()), { days: 30 }), endDate: endOfDay(new Date()) }, + { name: 'Ovaj mjesec', startDate: startOfMonth(new Date()), endDate: endOfDay(new Date()) }, + { name: 'Prošli mjesec', startDate: sub(startOfMonth(new Date()), { months: 1 }), endDate: endOfMonth(sub(startOfMonth(new Date()), { months: 1 })) } ]; - const startValueStringFormat = hideTime || moment(propStartDate).startOf('day').isSame(propStartDate, 'minute') - ? 'DD.MM.YYYY.' - : 'DD.MM.YYYY. HH:mm'; - const endValueStringFormat = hideTime || moment(propEndDate).endOf('day').isSame(propEndDate, 'minute') - ? 'DD.MM.YYYY.' - : 'DD.MM.YYYY. HH:mm'; - const valueString = `${moment(propStartDate).format(startValueStringFormat)} do ${moment(propEndDate).format(endValueStringFormat)}`; + const startValueStringFormat = hideTime || isSameMinute(startOfDay(propStartDate), propStartDate) + ? 'dd.MM.yyyy.' + : 'dd.MM.yyyy. HH:mm'; + const endValueStringFormat = hideTime || isSameMinute(endOfDay(propEndDate), propEndDate) + ? 'dd.MM.yyyy.' + : 'dd.MM.yyyy. HH:mm'; + const valueString = `${format(propStartDate, startValueStringFormat)} do ${format(propEndDate, endValueStringFormat)}`; return ( <> @@ -172,7 +186,7 @@ export function DateTimeRangePicker({ variant="outlined" fullWidth title={`${opt.startDate} ${opt.endDate}`} - onClick={() => handlePreselect(opt.startDate.toDate(), opt.endDate.toDate())} + onClick={() => handlePreselect(opt.startDate, opt.endDate)} > {opt.name} @@ -191,7 +205,7 @@ export function DateTimeRangePicker({ setDateValue([e.target.value as unknown as Date, dateValue[1]])} fullWidth size="small" @@ -204,7 +218,7 @@ export function DateTimeRangePicker({ setDateValue([dateValue[0], e.target.value as unknown as Date])} fullWidth size="small" From b775bcac39b0359aec8def3bd1a4d0be3914f798 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 21:14:14 +0200 Subject: [PATCH 4/9] chore: Removed moment dependency --- apps/docs/package.json | 1 - .../DateTimeRangePicker.tsx | 2 +- packages/react-ui/package.json | 1 - packages/react-ui/temp/react-ui.api.md | 32 +++++++++---------- pnpm-lock.yaml | 26 ++++----------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/apps/docs/package.json b/apps/docs/package.json index 0d36190d..3d874aec 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -30,7 +30,6 @@ "@mui/material": "^5.14.8", "classix": "^2.1.34", "comment-parser": "^1.4.0", - "moment": "^2.29.4", "next": "13.4.19", "nextra": "latest", "nextra-theme-docs": "latest", diff --git a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx index f92203bb..defe0ab3 100644 --- a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -68,7 +68,7 @@ export function DateTimeRangePicker({ }; /** - * Combines the date and time into moment objects. + * Combines the date and time into Date objects. * * @returns First element is start date and time, seconds is end date and time. */ diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index b2aeb792..11e5c070 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -37,7 +37,6 @@ "@types/react-dom": "^18.2.0", "date-fns": "^2.30.0", "eslint-config-custom": "workspace:*", - "moment": "^2.29.4", "react": "^18.2.0", "rimraf": "^5.0.1", "tsconfig": "workspace:*", diff --git a/packages/react-ui/temp/react-ui.api.md b/packages/react-ui/temp/react-ui.api.md index 073fecb6..18d3968f 100644 --- a/packages/react-ui/temp/react-ui.api.md +++ b/packages/react-ui/temp/react-ui.api.md @@ -9,37 +9,37 @@ import { ButtonProps } from '@mui/material'; import { ComponentProps } from 'react'; import { DialogProps } from '@mui/material'; import { HTMLAttributes } from 'react'; -import { Moment } from 'moment'; import * as react_jsx_runtime from 'react/jsx-runtime'; import { ReactElement } from 'react'; import { TextField } from '@mui/material'; // @public -export function DateTimeRangePicker({ start: propStartDate, end: propEndDate, hideTime, onChange, ...rest }: DateTimeRangePickerProps): react_jsx_runtime.JSX.Element; - -// @public -export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title"> & { - start: Moment; - end: Moment; - onChange: (startDate: Moment, endDate: Moment) => void; - hideTime?: boolean; - dense?: boolean; -}; - -// @public -export function ConfirmDialog({ isOpen, message, maxWidth, fullWidth, color, confirmButtonText, cancelButtonText, onConfirm, onCancel, ...rest }: ConfirmDialogProps): react_jsx_runtime.JSX.Element; +export function ConfirmDialog({ isOpen, header, message, maxWidth, fullWidth, color, confirmButtonText, cancelButtonText, onConfirm, onCancel, ...rest }: ConfirmDialogProps): react_jsx_runtime.JSX.Element; // @public export type ConfirmDialogProps = Omit & { isOpen: boolean; - message: string; - color: ComponentProps['color']; + header: string; + message?: string; + color?: ComponentProps['color']; confirmButtonText?: string; cancelButtonText?: string; onConfirm: () => void; onCancel: () => void; }; +// @public +export function DateTimeRangePicker({ start: propStartDate, end: propEndDate, hideTime, onChange, ...rest }: DateTimeRangePickerProps): react_jsx_runtime.JSX.Element; + +// @public +export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title" | "onChange"> & { + start: Date; + end: Date; + onChange: (startDate: Date, endDate: Date) => void; + hideTime?: boolean; + dense?: boolean; +}; + // @public export function DropdownButton({ options, onClick, icon, ...rest }: DropdownButtonProps): react_jsx_runtime.JSX.Element; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4a0b993..2aa5d8a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,6 @@ importers: comment-parser: specifier: ^1.4.0 version: 1.4.0 - moment: - specifier: ^2.29.4 - version: 2.29.4 next: specifier: 13.4.19 version: 13.4.19(react-dom@18.2.0)(react@18.2.0) @@ -181,7 +178,7 @@ importers: version: 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react@18.2.0) '@mui/x-date-pickers-pro': specifier: 5.0.20 - version: 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: ^20.5.2 version: 20.5.2 @@ -197,9 +194,6 @@ importers: eslint-config-custom: specifier: workspace:* version: link:../eslint-config-custom - moment: - specifier: ^2.29.4 - version: 2.29.4 react: specifier: ^18.2.0 version: 18.2.0 @@ -504,7 +498,7 @@ packages: '@date-io/core': 2.17.0 dev: true - /@date-io/moment@2.17.0(moment@2.29.4): + /@date-io/moment@2.17.0: resolution: {integrity: sha512-e4nb4CDZU4k0WRVhz1Wvl7d+hFsedObSauDHKtZwU9kt7gdYEAzKgnrSCTHsEaXrDumdrkCYTeZ0Tmyk7uV4tw==} peerDependencies: moment: ^2.24.0 @@ -513,7 +507,6 @@ packages: optional: true dependencies: '@date-io/core': 2.17.0 - moment: 2.29.4 dev: true /@emotion/babel-plugin@11.11.0: @@ -1233,7 +1226,7 @@ packages: react: 18.2.0 react-is: 18.2.0 - /@mui/x-date-pickers-pro@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers-pro@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-syNnokcG4lvj9SIcnfji6eVvpSpuHs+7jqLsJYDBTogxK53ctSXQVMb5GQ16ioTzEQSqZojhngHSj+/s11Ao1Q==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1259,15 +1252,14 @@ packages: '@date-io/date-fns': 2.17.0(date-fns@2.30.0) '@date-io/dayjs': 2.17.0 '@date-io/luxon': 2.17.0 - '@date-io/moment': 2.17.0(moment@2.29.4) + '@date-io/moment': 2.17.0 '@mui/material': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) '@mui/system': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react@18.2.0) '@mui/utils': 5.14.8(react@18.2.0) - '@mui/x-date-pickers': 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0) '@mui/x-license-pro': 5.17.12(react@18.2.0) clsx: 1.2.1 date-fns: 2.30.0 - moment: 2.29.4 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1278,7 +1270,7 @@ packages: - '@emotion/styled' dev: true - /@mui/x-date-pickers@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers@5.0.20(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.8)(@mui/system@5.14.8)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ERukSeHIoNLbI1C2XRhF9wRhqfsr+Q4B1SAw2ZlU7CWgcG8UBOxgqRKDEOVAIoSWL+DWT6GRuQjOKvj6UXZceA==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1311,7 +1303,7 @@ packages: '@date-io/date-fns': 2.17.0(date-fns@2.30.0) '@date-io/dayjs': 2.17.0 '@date-io/luxon': 2.17.0 - '@date-io/moment': 2.17.0(moment@2.29.4) + '@date-io/moment': 2.17.0 '@emotion/react': 11.11.1(@types/react@18.2.21)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.21)(react@18.2.0) '@mui/material': 5.14.8(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) @@ -1320,7 +1312,6 @@ packages: '@types/react-transition-group': 4.4.6 clsx: 1.2.1 date-fns: 2.30.0 - moment: 2.29.4 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -6143,9 +6134,6 @@ packages: minimist: 1.2.8 dev: true - /moment@2.29.4: - resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} - /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} From aa2b5d6b0a9dd835bb129ac13f640cf00f342a61 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 21:15:03 +0200 Subject: [PATCH 5/9] Update ExampleDateTimeRangePicker.tsx --- apps/docs/components/ExampleDateTimeRangePicker.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/components/ExampleDateTimeRangePicker.tsx b/apps/docs/components/ExampleDateTimeRangePicker.tsx index fc269df4..3daffc92 100644 --- a/apps/docs/components/ExampleDateTimeRangePicker.tsx +++ b/apps/docs/components/ExampleDateTimeRangePicker.tsx @@ -6,9 +6,11 @@ export function ExampleDateTimeRangePicker() { const [end, setEnd] = useState(new Date()); return ( + // @highlight-start { setStart(start); setEnd(end); }} /> + // @highlight-end ) } \ No newline at end of file From 8a9d8ad7cf8dd09d1839926436e8f0b5a771b543 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 22:02:38 +0200 Subject: [PATCH 6/9] fix: Issued with parsing and time zones --- .../DateTimeRangePicker.tsx | 107 ++++++++++++------ .../DateTimeRangePickerInput.tsx | 29 +++++ 2 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 packages/react-ui/DateTimeRangePicker/DateTimeRangePickerInput.tsx diff --git a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx index defe0ab3..a1f41c91 100644 --- a/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/packages/react-ui/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -1,12 +1,39 @@ import { Box, Button, Grid, Popover, TextField, useTheme, useMediaQuery } from '@mui/material'; -import { ComponentProps, MouseEvent, useState } from 'react'; +import { ComponentProps, MouseEvent, useEffect, useMemo, useState } from 'react'; import { StaticDateRangePicker, LocalizationProvider } from '@mui/x-date-pickers-pro'; import { AdapterDateFns } from '@mui/x-date-pickers-pro/AdapterDateFns'; import hrLocale from 'date-fns/locale/hr'; -import { parse, format, isSameDay, isSameMinute, startOfDay, endOfDay, startOfYesterday, endOfYesterday, sub, startOfMonth, endOfMonth } from 'date-fns'; +import { parse, format, isSameDay, startOfDay, endOfDay, startOfYesterday, endOfYesterday, sub, startOfMonth, endOfMonth, intervalToDuration } from 'date-fns'; import { TimeInput } from '../TimeInput'; +import { DateTimeRangePickerInput } from './DateTimeRangePickerInput'; + +// TODO: Test cases +// - Test if start date format shows only date when time is 00:00 +// - Test if end date format shows only date when time is 23:59 +// - Test if start date and start time is combined correctly (without time-zone offsets) +// - Test if end date and end time is combined correctly (without time-zone offsets) + +function durationGetTime(duration: Duration) { + return (typeof duration.years !== 'undefined' ? duration.years * 365 * 24 * 60 * 60 * 1000 : 0) + + (typeof duration.months !== 'undefined' ? duration.months * 30 * 24 * 60 * 60 * 1000 : 0) + + (typeof duration.weeks !== 'undefined' ? duration.weeks * 7 * 24 * 60 * 60 * 1000 : 0) + + (typeof duration.days !== 'undefined' ? duration.days * 24 * 60 * 60 * 1000 : 0) + + (typeof duration.hours !== 'undefined' ? duration.hours * 60 * 60 * 1000 : 0) + + (typeof duration.minutes !== 'undefined' ? duration.minutes * 60 * 1000 : 0) + + (typeof duration.seconds !== 'undefined' ? duration.seconds * 1000 : 0); +} + +/** + * The date time range picker preselect option. + * @public + */ +export type DateTimeRangePickerPreselectOption = { + name: string, + startDate: Date, + endDate: Date +}; /** * The date time range picker props. @@ -15,9 +42,11 @@ import { TimeInput } from '../TimeInput'; export type DateTimeRangePickerProps = Omit, "onClick" | "value" | "title" | "onChange"> & { start: Date; end: Date; - onChange: (startDate: Date, endDate: Date) => void; hideTime?: boolean; + useSeconds?: boolean; dense?: boolean; + preselectOptions?: DateTimeRangePickerPreselectOption[]; + onChange: (startDate: Date, endDate: Date) => void; }; /** @@ -28,24 +57,33 @@ export type DateTimeRangePickerProps = Omit, "o * @public */ export function DateTimeRangePicker({ - start: propStartDate, - end: propEndDate, + start, + end, hideTime, + useSeconds, + preselectOptions, onChange, ...rest }: DateTimeRangePickerProps) { const [anchorEl, setAnchorEl] = useState(null); - const [startTime, setStartTime] = useState(format(propStartDate, 'HH:mm')); - const [endTime, setEndTime] = useState(format(propEndDate, 'HH:mm')); + const [startTime, setStartTime] = useState(format(start, 'HH:mm')); + const [endTime, setEndTime] = useState(format(end, 'HH:mm')); // Use js dates because we use date-fns adapter - const defaultStartDate = startOfDay(propStartDate); - const defaultEndDate = startOfDay(propEndDate); + const defaultStartDate = startOfDay(start); + const defaultEndDate = startOfDay(end); const [dateValue, setDateValue] = useState<[Date | null, Date | null]>([defaultStartDate, defaultEndDate]); const theme = useTheme(); const isDesktop = useMediaQuery(theme.breakpoints.up('sm')); + // Reset cached value when props change (only when not in popover) + useEffect(() => { + if (!anchorEl) { + setDateValue([defaultStartDate, defaultEndDate]); + } + }, [start, end]); + /** * Handles the picker input click. * This will open picker popover. @@ -63,8 +101,8 @@ export function DateTimeRangePicker({ const handleClose = () => { setAnchorEl(null); setDateValue([defaultStartDate, defaultEndDate]); - setStartTime(format(propStartDate, 'HH:mm')); - setEndTime(format(propEndDate, 'HH:mm')); + setStartTime(format(start, 'HH:mm')); + setEndTime(format(end, 'HH:mm')); }; /** @@ -76,24 +114,24 @@ export function DateTimeRangePicker({ if (dateValue[0] == null || dateValue[1] == null) return []; - let startDuration = parse(startTime, "HH:mm", 0); - const endDuration = parse(endTime, "HH:mm", 0); + let startDurationTime = durationGetTime(intervalToDuration({ start: startOfDay(new Date()), end: parse(startTime, "HH:mm", new Date()) })); + const endDurationTime = durationGetTime(intervalToDuration({ start: startOfDay(new Date()), end: parse(endTime, "HH:mm", new Date()) })); // Check if start is after end - set equal if (dateValue[0] != null && dateValue[1] != null && isSameDay(dateValue[0], dateValue[1]) - && startDuration.getTime() > endDuration.getTime()) { - startDuration = endDuration; + && startDurationTime > endDurationTime) { + startDurationTime = endDurationTime; } const startDateTime = dateValue[0] ? startOfDay(dateValue[0]) : undefined; if (startDateTime != null) { - startDateTime.setMilliseconds(startDateTime.getMilliseconds() + startDuration.getTime()); + startDateTime.setMilliseconds(startDateTime.getMilliseconds() + startDurationTime); } const endDateTime = dateValue[1] ? startOfDay(dateValue[1]) : undefined; if (endDateTime != null) { - endDateTime.setMilliseconds(endDateTime.getMilliseconds() + endDuration.getTime()); + endDateTime.setMilliseconds(endDateTime.getMilliseconds() + endDurationTime); } return [startDateTime, endDateTime]; }; @@ -123,26 +161,23 @@ export function DateTimeRangePicker({ const open = Boolean(anchorEl); const id = open ? 'simple-popover' : undefined; - const preselectedOptions: { name: string, startDate: Date, endDate: Date }[] = [ - { name: 'Danas', startDate: startOfDay(new Date()), endDate: endOfDay(new Date()) }, - { name: 'Jučer', startDate: startOfYesterday(), endDate: endOfYesterday() }, - { name: 'Proteklih 7 dana', startDate: sub(startOfDay(new Date()), { days: 7 }), endDate: endOfDay(new Date()) }, - { name: 'Proteklih 30 dana', startDate: sub(startOfDay(new Date()), { days: 30 }), endDate: endOfDay(new Date()) }, - { name: 'Ovaj mjesec', startDate: startOfMonth(new Date()), endDate: endOfDay(new Date()) }, - { name: 'Prošli mjesec', startDate: sub(startOfMonth(new Date()), { months: 1 }), endDate: endOfMonth(sub(startOfMonth(new Date()), { months: 1 })) } - ]; + const preselectedOptionsOrDefault: { name: string, startDate: Date, endDate: Date }[] = useMemo(() => { + if (preselectOptions) + return preselectOptions; - const startValueStringFormat = hideTime || isSameMinute(startOfDay(propStartDate), propStartDate) - ? 'dd.MM.yyyy.' - : 'dd.MM.yyyy. HH:mm'; - const endValueStringFormat = hideTime || isSameMinute(endOfDay(propEndDate), propEndDate) - ? 'dd.MM.yyyy.' - : 'dd.MM.yyyy. HH:mm'; - const valueString = `${format(propStartDate, startValueStringFormat)} do ${format(propEndDate, endValueStringFormat)}`; + return [ + { name: 'Danas', startDate: startOfDay(new Date()), endDate: endOfDay(new Date()) }, + { name: 'Jučer', startDate: startOfYesterday(), endDate: endOfYesterday() }, + { name: 'Proteklih 7 dana', startDate: sub(startOfDay(new Date()), { days: 7 }), endDate: endOfDay(new Date()) }, + { name: 'Proteklih 30 dana', startDate: sub(startOfDay(new Date()), { days: 30 }), endDate: endOfDay(new Date()) }, + { name: 'Ovaj mjesec', startDate: startOfMonth(new Date()), endDate: endOfDay(new Date()) }, + { name: 'Prošli mjesec', startDate: sub(startOfMonth(new Date()), { months: 1 }), endDate: endOfMonth(sub(startOfMonth(new Date()), { months: 1 })) } + ]; + }, [preselectOptions]); return ( <> - + - {preselectedOptions.map((opt) => ( + {preselectedOptionsOrDefault.map((opt) => ( - - - - - - - {preselectedOptionsOrDefault.map((opt) => ( - - - - ))} - - + + + + + + {preselectedOptionsOrDefault.map((opt) => ( + + ))} + - + + + setDateValue([e.target.value as unknown as Date, dateValue[1]])} + fullWidth + size="small" + label="Od" + InputLabelProps={{ + shrink: true, + }} + /> + setDateValue([dateValue[0], e.target.value as unknown as Date])} + fullWidth + size="small" + label="Do" + InputLabelProps={{ + shrink: true, + }} + /> + {!hideTime && ( - <> - - - - - setDateValue([e.target.value as unknown as Date, dateValue[1]])} - fullWidth - size="small" - label="Od" - InputLabelProps={{ - shrink: true, - }} - /> - - - setDateValue([dateValue[0], e.target.value as unknown as Date])} - fullWidth - size="small" - label="Do" - InputLabelProps={{ - shrink: true, - }} - /> - - - - - - - - - - - - - - - - - + + + + )} - - - ( - <> - - do - - - )} - /> - - - + + ( + + + + + )} + /> + + From 99d92a045909258def114a3ad0ddcb8e2fa69d25 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 23:15:25 +0200 Subject: [PATCH 8/9] feat: TimeInput added enter key handling --- packages/react-ui/TimeInput/TimeInput.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/react-ui/TimeInput/TimeInput.tsx b/packages/react-ui/TimeInput/TimeInput.tsx index 2d1254df..d83acc30 100644 --- a/packages/react-ui/TimeInput/TimeInput.tsx +++ b/packages/react-ui/TimeInput/TimeInput.tsx @@ -49,6 +49,7 @@ export function TimeInput({ defaultValue, useSeconds, onChange, + onKeyDown, onBlur, onTimeChange, ...rest @@ -79,11 +80,25 @@ export function TimeInput({ } } + /** + * Handles the input key down. + * This will call onTimeChange callback if the key is Enter. + */ + function handleKeyDown(event: React.KeyboardEvent) { + if (onKeyDown) { + onKeyDown(event); + } + if (event.key === 'Enter' && onTimeChange) { + onTimeChange(processTime(value ?? defaultValue, useSeconds ?? false)); + } + } + return ( From 6daf406e8e9891b723fcdcf7637be974a5d0d4bc Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Fri, 15 Sep 2023 14:59:06 +0200 Subject: [PATCH 9/9] feat: Added ability to customize color --- apps/docs/components/theme.ts | 2 +- packages/react-ui/PageDrawer/PageDrawer.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/docs/components/theme.ts b/apps/docs/components/theme.ts index c9338213..cd4f821a 100644 --- a/apps/docs/components/theme.ts +++ b/apps/docs/components/theme.ts @@ -4,7 +4,7 @@ export const theme = createTheme({ palette: { mode: 'dark', primary: { - main: '#ffffff', + main: '#eeeeee', }, secondary: { main: '#b5b5b5', diff --git a/packages/react-ui/PageDrawer/PageDrawer.tsx b/packages/react-ui/PageDrawer/PageDrawer.tsx index e6544db6..f2388797 100644 --- a/packages/react-ui/PageDrawer/PageDrawer.tsx +++ b/packages/react-ui/PageDrawer/PageDrawer.tsx @@ -9,6 +9,7 @@ import { useDebounce, useResizeObserver } from '@enterwell/react-hooks'; * @public */ export type PageDrawerProps = HTMLAttributes & { + color?: string; expanded?: boolean; onChange?: () => void; }; @@ -20,7 +21,7 @@ export type PageDrawerProps = HTMLAttributes & { * @returns The PageDrawer component. * @public */ -export function PageDrawer({ expanded, onChange, children, ...rest }: PageDrawerProps): JSX.Element { +export function PageDrawer({ expanded, onChange, children, color, ...rest }: PageDrawerProps): JSX.Element { const [drawerSize, setDrawerSize] = useState(0); const drawerSizeDebounced = useDebounce(drawerSize, 50); const resizeObserverRef = useResizeObserver((_, entry) => { @@ -34,9 +35,7 @@ export function PageDrawer({ expanded, onChange, children, ...rest }: PageDrawer ref={resizeObserverRef} sx={{ position: 'absolute', - bottom: 0, - left: 0, - right: 0, + inset: 'auto 0 0 0', bgcolor: 'transparent', backgroundImage: 'none', border: 'none', @@ -55,8 +54,8 @@ export function PageDrawer({ expanded, onChange, children, ...rest }: PageDrawer minHeight: 32, height: 32, '.MuiAccordionSummary-expandIconWrapper': { - bgcolor: 'rgba(234, 135, 20, 0.2)', - color: '#EA8714', + bgcolor: color ?? 'primary.dark', + color: 'primary.main', borderRadius: 1 }, '&.Mui-expanded': { @@ -77,8 +76,9 @@ export function PageDrawer({ expanded, onChange, children, ...rest }: PageDrawer >