From 9ee7b92bf88c1b34dbfd3d4a9d1011c7f27b9081 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Mon, 3 Feb 2025 17:42:18 +0100 Subject: [PATCH 1/4] feat: add dayjs and react-native-ui-datepicker dependencies --- package.json | 2 ++ yarn.lock | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/package.json b/package.json index 5331031..1d0568b 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@tanstack/react-query": "^5.64.1", "axios": "^1.7.9", "core-js": "^3.37.1", + "dayjs": "^1.11.13", "expo": "52.0.27", "expo-apple-authentication": "~7.1.3", "expo-application": "~6.0.2", @@ -163,6 +164,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "15.8.0", + "react-native-ui-datepicker": "^2.0.11", "react-native-web": "~0.19.6", "reactotron-react-native": "^5.1.7", "sass": "^1.77.2", diff --git a/yarn.lock b/yarn.lock index 140e6fd..d47c371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1273,6 +1273,7 @@ __metadata: commander: "npm:^12.1.0" core-js: "npm:^3.37.1" cross-env: "npm:^7.0.3" + dayjs: "npm:^1.11.13" dotenv: "npm:^16.4.7" enquirer: "npm:^2.4.1" eslint: "npm:^9.9.1" @@ -1340,6 +1341,7 @@ __metadata: react-native-safe-area-context: "npm:4.12.0" react-native-screens: "npm:~4.4.0" react-native-svg: "npm:15.8.0" + react-native-ui-datepicker: "npm:^2.0.11" react-native-url-polyfill: "npm:^2.0.0" react-native-web: "npm:~0.19.6" react-test-renderer: "npm:^18.3.1" @@ -6309,6 +6311,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.13": + version: 1.11.13 + resolution: "dayjs@npm:1.11.13" + checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -13317,6 +13326,20 @@ __metadata: languageName: node linkType: hard +"react-native-ui-datepicker@npm:^2.0.11": + version: 2.0.11 + resolution: "react-native-ui-datepicker@npm:2.0.11" + dependencies: + dayjs: "npm:^1.11.13" + lodash: "npm:^4.17.21" + peerDependencies: + react: "*" + react-native: "*" + react-native-web: "*" + checksum: 10/4374f45cc29111f8f253d5b2061af29726d88f1ba7eebba32db5736193de4bcb190d248f8bf773d44cfe41cebaf4bf6f0eae1a334c3387c1c915521e6587af44 + languageName: node + linkType: hard + "react-native-url-polyfill@npm:^2.0.0": version: 2.0.0 resolution: "react-native-url-polyfill@npm:2.0.0" From f650e4133cacecde5fbf8ca77a94480711871a5b Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Thu, 6 Feb 2025 11:05:02 +0100 Subject: [PATCH 2/4] fix: downgrade react-native-ui-datepicker to version 2.0.4 --- package.json | 2 +- yarn.lock | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1d0568b..6cbda4f 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "15.8.0", - "react-native-ui-datepicker": "^2.0.11", + "react-native-ui-datepicker": "2.0.4", "react-native-web": "~0.19.6", "reactotron-react-native": "^5.1.7", "sass": "^1.77.2", diff --git a/yarn.lock b/yarn.lock index d47c371..6b118e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1341,7 +1341,7 @@ __metadata: react-native-safe-area-context: "npm:4.12.0" react-native-screens: "npm:~4.4.0" react-native-svg: "npm:15.8.0" - react-native-ui-datepicker: "npm:^2.0.11" + react-native-ui-datepicker: "npm:2.0.4" react-native-url-polyfill: "npm:^2.0.0" react-native-web: "npm:~0.19.6" react-test-renderer: "npm:^18.3.1" @@ -13326,17 +13326,17 @@ __metadata: languageName: node linkType: hard -"react-native-ui-datepicker@npm:^2.0.11": - version: 2.0.11 - resolution: "react-native-ui-datepicker@npm:2.0.11" +"react-native-ui-datepicker@npm:2.0.4": + version: 2.0.4 + resolution: "react-native-ui-datepicker@npm:2.0.4" dependencies: dayjs: "npm:^1.11.13" lodash: "npm:^4.17.21" + react-native-wheely: "npm:^0.6.0" peerDependencies: react: "*" react-native: "*" - react-native-web: "*" - checksum: 10/4374f45cc29111f8f253d5b2061af29726d88f1ba7eebba32db5736193de4bcb190d248f8bf773d44cfe41cebaf4bf6f0eae1a334c3387c1c915521e6587af44 + checksum: 10/39da674809a855713cf97edfb2da1bfea24b9e7ea1ab76facc76fac6679efb69b94cb78f5f42589131408c1887981f273c4f4a0e7a925891571d1ef8274c0a1b languageName: node linkType: hard @@ -13370,6 +13370,13 @@ __metadata: languageName: node linkType: hard +"react-native-wheely@npm:^0.6.0": + version: 0.6.0 + resolution: "react-native-wheely@npm:0.6.0" + checksum: 10/c945e78f262f739da233f4348963c9fde976ec1a7cb2fd557e273db4d57dfa65e1e1b42f444d42158b940184ceb8c2fcd985001df57d9aedc238aec1ca4ba13c + languageName: node + linkType: hard + "react-native@npm:0.76.6": version: 0.76.6 resolution: "react-native@npm:0.76.6" From 5ebbd548498b6e9390b48970d9d3ab01fe5a81a8 Mon Sep 17 00:00:00 2001 From: Mateusz Rostkowski Date: Thu, 6 Feb 2025 11:05:38 +0100 Subject: [PATCH 3/4] feat: add datepicker component --- src/components/molecules/Field/Field.tsx | 3 + .../molecules/Field/FieldDatePicker.tsx | 78 +++++++ src/components/molecules/Field/types.ts | 10 + .../ControlledField/ControlledDatePicker.tsx | 44 ++++ .../ControlledField/ControlledField.tsx | 3 + .../organisms/ControlledField/types.ts | 11 + src/design-system/components/DatePicker.tsx | 193 ++++++++++++++++++ src/design-system/components/index.ts | 1 + src/design-system/components/types.ts | 19 ++ 9 files changed, 362 insertions(+) create mode 100644 src/components/molecules/Field/FieldDatePicker.tsx create mode 100644 src/components/organisms/ControlledField/ControlledDatePicker.tsx create mode 100644 src/design-system/components/DatePicker.tsx diff --git a/src/components/molecules/Field/Field.tsx b/src/components/molecules/Field/Field.tsx index 580ef54..07caf43 100644 --- a/src/components/molecules/Field/Field.tsx +++ b/src/components/molecules/Field/Field.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { FieldCheckbox } from './FieldCheckbox' import { FieldCheckboxGroup } from './FieldCheckboxGroup' +import { FieldDatePicker } from './FieldDatePicker' import { FieldInput } from './FieldInput' import { FieldRadioGroup } from './FieldRadioGroup' import { FieldSelect } from './FieldSelect' @@ -12,6 +13,7 @@ type FieldComposition = React.FC & { Checkbox: typeof FieldCheckbox RadioGroup: typeof FieldRadioGroup Select: typeof FieldSelect + DatePicker: typeof FieldDatePicker } const Field: FieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ Field.CheckboxGroup = FieldCheckboxGroup Field.Checkbox = FieldCheckbox Field.RadioGroup = FieldRadioGroup Field.Select = FieldSelect +Field.DatePicker = FieldDatePicker export { Field } export * from './types' diff --git a/src/components/molecules/Field/FieldDatePicker.tsx b/src/components/molecules/Field/FieldDatePicker.tsx new file mode 100644 index 0000000..eb1e61a --- /dev/null +++ b/src/components/molecules/Field/FieldDatePicker.tsx @@ -0,0 +1,78 @@ +import { forwardRef, useCallback, useImperativeHandle, useRef, useMemo } from 'react' +import { NativeSyntheticEvent, TextInputFocusEventData } from 'react-native' + +import type { FieldDatePickerProps } from './types' + +import { + FormErrorMessage, + FormLabel, + Box, + DatePicker, + DatePickerRef, +} from '@/design-system/components' +import { getLayoutProps } from '@/design-system/utils/getLayoutProps' + +export const FieldDatePicker = forwardRef( + ( + { + errorMessage, + isInvalid, + isRequired, + label, + labelStyle, + onBlur, + onFocus, + testID, + onChangeDate, + ...props + }, + ref + ) => { + const _datePickerRef = useRef(null) + + const { layoutProps, restProps: datePickerProps } = useMemo( + () => getLayoutProps(props), + [props] + ) + + const handleFocus = useCallback(() => { + _datePickerRef?.current?.focus() + }, [onFocus]) + + const handleBlur = useCallback( + (e?: NativeSyntheticEvent) => { + onBlur && e && onBlur(e) + _datePickerRef.current?.blur() + }, + [onBlur] + ) + + useImperativeHandle( + ref, + () => ({ + focus: handleFocus, + blur: handleBlur, + ..._datePickerRef.current, + }), + [handleBlur, handleFocus] + ) + + return ( + + + + + + ) + } +) diff --git a/src/components/molecules/Field/types.ts b/src/components/molecules/Field/types.ts index acde71c..7f3f2a0 100644 --- a/src/components/molecules/Field/types.ts +++ b/src/components/molecules/Field/types.ts @@ -7,6 +7,7 @@ import { SelectProps, StyledProps, TouchableRef, + DatePickerProps, } from '@/design-system' // ----------------------- @@ -92,3 +93,12 @@ export type FieldCheckboxProps = FormLabelProps & CheckboxProps & { errorMessage?: string } + +// ----------------------- +// ----- DATEPICKER ------ +// ----------------------- + +export type FieldDatePickerProps = FormLabelProps & + DatePickerProps & { + errorMessage?: string + } diff --git a/src/components/organisms/ControlledField/ControlledDatePicker.tsx b/src/components/organisms/ControlledField/ControlledDatePicker.tsx new file mode 100644 index 0000000..da6aa99 --- /dev/null +++ b/src/components/organisms/ControlledField/ControlledDatePicker.tsx @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { Controller, ControllerProps, FieldValues, get } from 'react-hook-form' + +import type { ControlledDatePickerProps } from './types' +import { Field } from '../../molecules' + +export const ControlledDatePicker = ({ + control, + name, + errors, + rules, + children, + ...props +}: ControlledDatePickerProps) => { + const errorMessage = get(errors, name)?.message + + const renderDatePicker = useCallback( + ({ + field: { onChange, name, ref, value, ...fieldProps }, + }: Parameters[0]) => { + return ( + + ) + }, + [errorMessage, props] + ) + + return ( + + ) +} diff --git a/src/components/organisms/ControlledField/ControlledField.tsx b/src/components/organisms/ControlledField/ControlledField.tsx index 64a2194..d59b8a5 100644 --- a/src/components/organisms/ControlledField/ControlledField.tsx +++ b/src/components/organisms/ControlledField/ControlledField.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren } from 'react' import { ControlledCheckbox } from './ControlledCheckbox' import { ControlledCheckboxGroup } from './ControlledCheckboxGroup' +import { ControlledDatePicker } from './ControlledDatePicker' import { ControlledInput } from './ControlledInput' import { ControlledRadioGroup } from './ControlledRadioGroup' import { ControlledSelect } from './ControlledSelect' @@ -12,6 +13,7 @@ type ControlledFieldComposition = React.FC & { RadioGroup: typeof ControlledRadioGroup Checkbox: typeof ControlledCheckbox Select: typeof ControlledSelect + DatePicker: typeof ControlledDatePicker } const ControlledField: ControlledFieldComposition = ({ children }) => { @@ -23,6 +25,7 @@ ControlledField.CheckboxGroup = ControlledCheckboxGroup ControlledField.Checkbox = ControlledCheckbox ControlledField.RadioGroup = ControlledRadioGroup ControlledField.Select = ControlledSelect +ControlledField.DatePicker = ControlledDatePicker export { ControlledField } export * from './types' diff --git a/src/components/organisms/ControlledField/types.ts b/src/components/organisms/ControlledField/types.ts index 1050d95..296c787 100644 --- a/src/components/organisms/ControlledField/types.ts +++ b/src/components/organisms/ControlledField/types.ts @@ -6,6 +6,7 @@ import { FieldRadioGroupProps, FieldSelectProps, FieldCheckboxProps, + FieldDatePickerProps, } from '@/components/molecules' // ----------------------- @@ -68,3 +69,13 @@ export type ControlledCheckboxProps & ControlledFieldProps + +// ----------------------- +// ----- DATEPICKER ------ +// ----------------------- + +export type ControlledDatePickerProps = Omit< + FieldDatePickerProps, + 'onChangeDate' | 'date' +> & + ControlledFieldProps diff --git a/src/design-system/components/DatePicker.tsx b/src/design-system/components/DatePicker.tsx new file mode 100644 index 0000000..dbbfaf2 --- /dev/null +++ b/src/design-system/components/DatePicker.tsx @@ -0,0 +1,193 @@ +import dayjs from 'dayjs' +import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react' +import { StyleSheet } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import DateTimePicker from 'react-native-ui-datepicker' +import { DatePickerSingleProps } from 'react-native-ui-datepicker/lib/typescript/src/DateTimePicker' + +import { Box } from './Box' +import { BoxWithShadow } from './BoxWithShadow' +import { Button } from './Button' +import { Column } from './Column' +import { Icon } from './Icon' +import { Row } from './Row' +import { Spacer } from './Spacer' +import { Text } from './Text' +import { Touchable } from './Touchables' +import { DatePickerProps, DatePickerRef } from './types' +import { useBottomSheet } from '../bottomSheets' + +import { useTheme, useTranslation } from '@/hooks' + +export type DateTypes = 'protected' | 'email' | 'light' | undefined + +type DatePickerModalProps = { + date?: Date + setDate(date: Date): void + onChangeDate: ((date: Date) => void) | undefined + maximumDate?: Date + minimumDate?: Date + onSave?: () => void +} + +const DatePickerModal = ({ date, setDate, onChangeDate, onSave }: DatePickerModalProps) => { + const { bottom } = useSafeAreaInsets() + const { colors } = useTheme() + const { t } = useTranslation() + + const [tempDate, setTempDate] = useState(date || new Date()) + + const onChange: NonNullable = useCallback((params) => { + if (!params.date) return + + const newDate = dayjs(params.date).toDate() + + setTempDate(newDate) + }, []) + + const handleSave = useCallback(() => { + setDate(tempDate) + onChangeDate?.(tempDate) + onSave?.() + }, [tempDate, setDate, onChangeDate]) + + const handleCancel = useCallback(() => { + setTempDate(date ?? new Date()) + onSave?.() + }, [date]) + + return ( + + + +