Skip to content

Commit

Permalink
feat(mobile): [Transaction] add transaction form layout (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 committed Jul 11, 2024
1 parent af42cec commit 738a1aa
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 22 deletions.
30 changes: 14 additions & 16 deletions apps/mobile/app/(app)/new-record.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { NumericPad } from '@/components/numeric-pad'
import { TextTicker } from '@/components/text-ticker'
import { useState } from 'react'
import { View } from 'react-native'
import { TransactionForm } from '@/components/transaction/transaction-form'
import { useWallets } from '@/queries/wallet'
import { useRouter } from 'expo-router'

export default function NewRecordScreen() {
const [value, setValue] = useState<number>(0)
const router = useRouter()
const { data: walletAccounts } = useWallets()

return (
<View className="flex-1 justify-between bg-muted">
<View className="flex-1 items-center justify-center">
<TextTicker
value={value}
className="font-semibold text-6xl leading-tight text-center"
suffix="VND"
suffixClassName="font-semibold ml-2 text-muted-foreground overflow-visible"
/>
</View>
<NumericPad value={value} onValueChange={setValue} />
</View>
<TransactionForm
onSubmit={(values) => console.log('submit', values)}
onCancel={router.back}
defaultValues={{
walletAccountId: walletAccounts?.[0].id,
currency: walletAccounts?.[0].preferredCurrency,
}}
/>
)
}
4 changes: 3 additions & 1 deletion apps/mobile/components/form-fields/input-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type InputFieldProps = TextInputProps & {
leftSection?: React.ReactNode
rightSection?: React.ReactNode
disabled?: boolean
wrapperClassName?: string
}

export const InputField = forwardRef(
Expand All @@ -21,6 +22,7 @@ export const InputField = forwardRef(
leftSection,
rightSection,
className,
wrapperClassName,
disabled,
...props
}: InputFieldProps,
Expand All @@ -31,7 +33,7 @@ export const InputField = forwardRef(
fieldState,
} = useController({ name })
return (
<View className="gap-1">
<View className={cn('gap-1', wrapperClassName)}>
{!!label && <Label nativeID={`label-${name}`}>{label}</Label>}
<View>
{leftSection && (
Expand Down
11 changes: 6 additions & 5 deletions apps/mobile/components/numeric-pad/numeric-pad.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cn } from '@/lib/utils'
import { DeleteIcon } from 'lucide-react-native'
import { View } from 'react-native'
import Animated from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Button } from '../ui/button'
import { Text } from '../ui/text'
Expand Down Expand Up @@ -50,15 +51,15 @@ export function NumericPad({
}

return (
<View
<Animated.View
className={cn(
'flex-wrap bg-card flex-row border-t border-border items-center content-center p-2',
'flex-wrap bg-card flex-row border-t border-border items-center content-center py-1.5 px-2',
className,
)}
style={{ paddingBottom: bottom }}
>
{buttonKeys.map((buttonKey) => (
<View key={buttonKey} className="w-[33.33%] p-2">
<View key={buttonKey} className="w-[33.33%] p-1.5">
<Button
disabled={disabled}
onPress={() => handleKeyPress(buttonKey)}
Expand All @@ -69,7 +70,7 @@ export function NumericPad({
</Button>
</View>
))}
<View className="w-[33.33%] p-2">
<View className="w-[33.33%] p-1.5">
<Button
disabled={disabled}
onPress={handleDelete}
Expand All @@ -80,6 +81,6 @@ export function NumericPad({
<DeleteIcon className="size-8 text-primary" />
</Button>
</View>
</View>
</Animated.View>
)
}
138 changes: 138 additions & 0 deletions apps/mobile/components/transaction/transaction-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
type TransactionFormValues,
zTransactionFormValues,
} from '@6pm/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import {
Calendar,
CreditCard,
LandPlot,
ShapesIcon,
XIcon,
} from 'lucide-react-native'
import { Controller, FormProvider, useForm } from 'react-hook-form'
import { ScrollView, View } from 'react-native'
import Animated, {
useAnimatedKeyboard,
useAnimatedStyle,
} from 'react-native-reanimated'
import { InputField } from '../form-fields/input-field'
import { SubmitButton } from '../form-fields/submit-button'
import { NumericPad } from '../numeric-pad'
import { TextTicker } from '../text-ticker'
import { Button } from '../ui/button'
import { Text } from '../ui/text'

type TransactionFormProps = {
onSubmit: (data: TransactionFormValues) => void
defaultValues?: Partial<TransactionFormValues>
onCancel?: () => void
}

export const TransactionForm = ({
onSubmit,
defaultValues,
onCancel,
}: TransactionFormProps) => {
const { i18n } = useLingui()

const keyboard = useAnimatedKeyboard()
const translateStyle = useAnimatedStyle(() => {
return {
transform: [{ translateY: keyboard.height.value }],
}
})

const transactionForm = useForm<TransactionFormValues>({
resolver: zodResolver(zTransactionFormValues),
defaultValues: {
date: new Date(),
amount: 0,
currency: 'VND',
note: '',
...defaultValues,
},
})

const amount = transactionForm.watch('amount')
const currency = transactionForm.watch('currency')

return (
<FormProvider {...transactionForm}>
<ScrollView
keyboardShouldPersistTaps="handled"
automaticallyAdjustKeyboardInsets
contentContainerClassName="flex-1 justify-between bg-muted"
>
<View className="flex-row justify-between items-center p-6 pb-0">
<Button variant="outline" className="!px-3">
<Calendar className="w-5 h-5 text-primary" />
<Text>Today</Text>
</Button>
<Button size="icon" variant="secondary" onPress={onCancel}>
<XIcon className="size-6 text-primary" />
</Button>
</View>
<View className="flex-1 items-center justify-center pb-12">
<View className="w-full h-24 justify-end mb-4">
<TextTicker
value={amount}
className="font-semibold text-6xl leading-tight text-center"
suffix={currency}
suffixClassName="font-semibold ml-2 text-muted-foreground overflow-visible"
/>
</View>
<Button variant="outline" size="sm" className="rounded-full">
<LandPlot className="w-5 h-5 text-primary" />
<Text className="text-muted-foreground">
{t(i18n)`No budget selected`}
</Text>
</Button>
<InputField
name="note"
placeholder={t(i18n)`transaction note`}
autoCapitalize="none"
className="truncate line-clamp-1 bg-transparent border-0"
placeholderClassName="!text-muted"
wrapperClassName="absolute left-4 right-4 bottom-2"
/>
</View>
<Animated.View style={translateStyle}>
<View className="flex-row items-center justify-between bg-card border-t border-border p-2">
<View className="flex-row items-center gap-2">
<Button
variant="secondary"
className="border border-border !px-3"
>
<CreditCard className="w-5 h-5 text-primary" />
<Text>{t(i18n)`Credit Card`}</Text>
</Button>
<Button
variant="secondary"
className="border border-border !px-3"
>
<ShapesIcon className="w-5 h-5 text-primary" />
<Text>{t(i18n)`Uncategorized`}</Text>
</Button>
</View>
<SubmitButton
onPress={transactionForm.handleSubmit(onSubmit)}
disabled={transactionForm.formState.isLoading || !amount}
>
<Text>{t(i18n)`Save`}</Text>
</SubmitButton>
</View>
<Controller
name="amount"
control={transactionForm.control}
render={({ field: { onChange, value } }) => (
<NumericPad value={value} onValueChange={onChange} />
)}
/>
</Animated.View>
</ScrollView>
</FormProvider>
)
}
3 changes: 3 additions & 0 deletions packages/validation/src/transaction.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export const zUpdateTransaction = z.object({
walletAccountId: z.string().optional(),
})
export type UpdateTransaction = z.infer<typeof zUpdateTransaction>

export const zTransactionFormValues = zCreateTransaction
export type TransactionFormValues = z.infer<typeof zTransactionFormValues>

0 comments on commit 738a1aa

Please sign in to comment.