From 31a14da3f3fac29a83fb82b3d8dfaab131a96ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BB=91c=20Kh=C3=A1nh?= Date: Wed, 10 Jul 2024 22:57:48 +0700 Subject: [PATCH] feat(mobile): [Transaction] add select date field --- .../transaction/select-date-field.tsx | 112 ++++++++++++++++++ .../transaction/transaction-form.tsx | 8 +- apps/mobile/lib/date.tsx | 26 ++++ apps/mobile/package.json | 2 + pnpm-lock.yaml | 28 ++++- 5 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 apps/mobile/components/transaction/select-date-field.tsx create mode 100644 apps/mobile/lib/date.tsx diff --git a/apps/mobile/components/transaction/select-date-field.tsx b/apps/mobile/components/transaction/select-date-field.tsx new file mode 100644 index 00000000..31ebd666 --- /dev/null +++ b/apps/mobile/components/transaction/select-date-field.tsx @@ -0,0 +1,112 @@ +import { formatDateShort } from '@/lib/date' +import { sleep } from '@/lib/utils' +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetView, +} from '@gorhom/bottom-sheet' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import DateTimePicker from '@react-native-community/datetimepicker' +import { Calendar } from 'lucide-react-native' +import { useRef, useState } from 'react' +import { useController } from 'react-hook-form' +import { View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { FullWindowOverlay } from 'react-native-screens' +import { Button } from '../ui/button' +import { Text } from '../ui/text' + +function SpinnerDatePicker({ + value, + onChange, +}: { + value: Date + onChange: (date: Date | undefined) => void +}) { + const { i18n } = useLingui() + const [date, setDate] = useState(value) + + return ( + + { + setDate(selectedDate) + }} + /> + + + ) +} + +export function SelectDateField({ + onSelect, +}: { + onSelect?: (date?: Date) => void +}) { + const { bottom } = useSafeAreaInsets() + const sheetRef = useRef(null) + const { + field: { onChange, onBlur, value }, + } = useController({ name: 'date' }) + + return ( + <> + + ( + + )} + containerComponent={(props) => ( + {props.children} + )} + > + + { + sheetRef.current?.close() + await sleep(500) + onChange(date) + onBlur() + onSelect?.(date) + }} + /> + + + + ) +} diff --git a/apps/mobile/components/transaction/transaction-form.tsx b/apps/mobile/components/transaction/transaction-form.tsx index 12e7c145..01ef3d57 100644 --- a/apps/mobile/components/transaction/transaction-form.tsx +++ b/apps/mobile/components/transaction/transaction-form.tsx @@ -5,7 +5,7 @@ import { import { zodResolver } from '@hookform/resolvers/zod' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' -import { Calendar, LandPlot, XIcon } from 'lucide-react-native' +import { LandPlot, XIcon } from 'lucide-react-native' import { Controller, FormProvider, useForm } from 'react-hook-form' import { ScrollView, View } from 'react-native' import Animated, { @@ -20,6 +20,7 @@ import { Button } from '../ui/button' import { Text } from '../ui/text' import { SelectAccountField } from './select-account-field' import { SelectCategoryField } from './select-category-field' +import { SelectDateField } from './select-date-field' type TransactionFormProps = { onSubmit: (data: TransactionFormValues) => void @@ -63,10 +64,7 @@ export const TransactionForm = ({ contentContainerClassName="flex-1 justify-between bg-muted" > - + diff --git a/apps/mobile/lib/date.tsx b/apps/mobile/lib/date.tsx new file mode 100644 index 00000000..fb8b7138 --- /dev/null +++ b/apps/mobile/lib/date.tsx @@ -0,0 +1,26 @@ +import { t } from '@lingui/macro' +import { format } from 'date-fns/format' +import { isSameYear } from 'date-fns/isSameYear' +import { isToday } from 'date-fns/isToday' +import { isTomorrow } from 'date-fns/isTomorrow' +import { isYesterday } from 'date-fns/isYesterday' + +export function formatDateShort(date: Date) { + if (isToday(date)) { + return t`Today` + } + + if (isYesterday(date)) { + return t`Yesterday` + } + + if (isTomorrow(date)) { + return t`Tomorrow` + } + + if (isSameYear(date, new Date())) { + return format(date, 'MMM d') + } + + return format(date, 'P') +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index f3e11f18..8b88e12f 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -33,10 +33,12 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@react-native-async-storage/async-storage": "^1.23.1", + "@react-native-community/datetimepicker": "8.0.1", "@react-navigation/native": "^6.0.2", "@tanstack/react-query": "^5.40.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^3.6.0", "expo": "~51.0.11", "expo-auth-session": "~5.5.2", "expo-constants": "~16.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4136dbea..1247e2f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: '@react-native-async-storage/async-storage': specifier: ^1.23.1 version: 1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1)) + '@react-native-community/datetimepicker': + specifier: 8.0.1 + version: 8.0.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) '@react-navigation/native': specifier: ^6.0.2 version: 6.1.17(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) @@ -147,6 +150,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 expo: specifier: ~51.0.11 version: 51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) @@ -170,7 +176,7 @@ importers: version: 15.0.3(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-router: specifier: ~3.5.15 - version: 3.5.16(4mulymdxn7fmzuvb6n5wc7wvuq) + version: 3.5.16(expo-constants@16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(typescript@5.3.3) expo-secure-store: specifier: ^13.0.1 version: 13.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) @@ -2030,6 +2036,16 @@ packages: engines: {node: '>=18'} hasBin: true + '@react-native-community/datetimepicker@8.0.1': + resolution: {integrity: sha512-4BO0t3geMNNw9cIIm9p9FNUzwMXexdzD4pAH0AaUAycs3BS71HLrX8jHbrI7nzq/+8O7cLAXn5Gudte+YpTV8Q==} + peerDependencies: + react: '*' + react-native: '*' + react-native-windows: '*' + peerDependenciesMeta: + react-native-windows: + optional: true + '@react-native/assets-registry@0.74.84': resolution: {integrity: sha512-dzUhwyaX04QosWZ8zyaaNB/WYZIdeDN1lcpfQbqiOhZJShRH+FLTDVONE/dqlMQrP+EO7lDqF0RrlIt9lnOCQQ==} engines: {node: '>=18'} @@ -8758,6 +8774,12 @@ snapshots: - supports-color - utf-8-validate + '@react-native-community/datetimepicker@8.0.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)': + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1) + '@react-native/assets-registry@0.74.84': {} '@react-native/babel-plugin-codegen@0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7))': @@ -10318,8 +10340,8 @@ snapshots: dependencies: invariant: 2.2.4 - expo-router@3.5.16(4mulymdxn7fmzuvb6n5wc7wvuq): - dependencies: + ? expo-router@3.5.16(expo-constants@16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(typescript@5.3.3) + : dependencies: '@expo/metro-runtime': 3.2.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1)) '@expo/server': 0.4.3(typescript@5.3.3) '@radix-ui/react-slot': 1.0.1(react@18.3.1)