diff --git a/package.json b/package.json index 5331031d..6cbda4ff 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.4", "react-native-web": "~0.19.6", "reactotron-react-native": "^5.1.7", "sass": "^1.77.2", diff --git a/src/components/molecules/Field/Field.tsx b/src/components/molecules/Field/Field.tsx index 580ef547..07caf432 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 00000000..eb1e61a5 --- /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 acde71ce..7f3f2a05 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 00000000..da6aa995 --- /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 64a21940..d59b8a51 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 1050d955..296c7871 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 00000000..dbbfaf28 --- /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 ( + + + +