-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mobile): add basic transaction amount input
- Loading branch information
Quốc Khánh
committed
Jul 10, 2024
1 parent
05bb0d8
commit bc5b42e
Showing
6 changed files
with
235 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,21 @@ | ||
import { Text } from 'react-native' | ||
import { NumericPad } from '@/components/numeric-pad' | ||
import { TextTicker } from '@/components/text-ticker' | ||
import { useState } from 'react' | ||
import { View } from 'react-native' | ||
|
||
export default function NewRecordScreen() { | ||
return <Text className="font-sans m-4 mx-auto">New Record</Text> | ||
const [value, setValue] = useState<number>(0) | ||
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './numeric-pad' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { cn } from '@/lib/utils' | ||
import { DeleteIcon } from 'lucide-react-native' | ||
import { View } from 'react-native' | ||
import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||
import { Button } from '../ui/button' | ||
import { Text } from '../ui/text' | ||
|
||
const buttonKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '000', '0'] | ||
|
||
type NumericPadProps = { | ||
disabled?: boolean | ||
value: number | ||
onValueChange?: (value: number) => void | ||
maxValue?: number | ||
className?: string | ||
} | ||
|
||
export function NumericPad({ | ||
disabled, | ||
value = 0, | ||
onValueChange, | ||
maxValue = 9999999999, | ||
className, | ||
}: NumericPadProps) { | ||
const { bottom } = useSafeAreaInsets() | ||
|
||
function handleKeyPress(key: string) { | ||
let newValue: number | ||
|
||
if (key === '000') { | ||
newValue = value * 1000 | ||
} else { | ||
newValue = value * 10 + Number(key) | ||
} | ||
|
||
if (newValue > maxValue) { | ||
return | ||
} | ||
|
||
onValueChange?.(newValue) | ||
} | ||
|
||
function handleDelete() { | ||
const newValue = Math.floor(value / 10) | ||
onValueChange?.(newValue) | ||
} | ||
|
||
function handleClear() { | ||
onValueChange?.(0) | ||
} | ||
|
||
return ( | ||
<View | ||
className={cn( | ||
'flex-wrap bg-card flex-row border-t border-border items-center content-center p-2', | ||
className, | ||
)} | ||
style={{ paddingBottom: bottom }} | ||
> | ||
{buttonKeys.map((buttonKey) => ( | ||
<View key={buttonKey} className="w-[33.33%] p-2"> | ||
<Button | ||
disabled={disabled} | ||
onPress={() => handleKeyPress(buttonKey)} | ||
variant="ghost" | ||
size="lg" | ||
> | ||
<Text className="!text-2xl">{buttonKey}</Text> | ||
</Button> | ||
</View> | ||
))} | ||
<View className="w-[33.33%] p-2"> | ||
<Button | ||
disabled={disabled} | ||
onPress={handleDelete} | ||
onLongPress={handleClear} | ||
variant="secondary" | ||
size="lg" | ||
> | ||
<DeleteIcon className="size-8 text-primary" /> | ||
</Button> | ||
</View> | ||
</View> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './text-ticker' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { cn } from '@/lib/utils' | ||
import React, { useState } from 'react' | ||
import type { TextStyle } from 'react-native' | ||
import Animated, { | ||
LinearTransition, | ||
SlideInDown, | ||
SlideOutDown, | ||
} from 'react-native-reanimated' | ||
import { Text } from '../ui/text' | ||
|
||
function formatNumberWithCommas(formatter: Intl.NumberFormat, num: number) { | ||
const formattedNum = formatter.format(num) | ||
const result: { value: string; key: string }[] = [] | ||
let commaCount = 0 | ||
|
||
for (let i = 0; i < formattedNum.length; i++) { | ||
const char = formattedNum[i] | ||
// We want to count the number of commas because we would like to | ||
// keep the index of the digits the same. | ||
if (char === ',') { | ||
result.push({ value: char, key: `comma-${i}` }) | ||
|
||
commaCount++ | ||
} else { | ||
result.push({ value: char, key: `digit-${i - commaCount}` }) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
type TextTickerProps = { | ||
style?: TextStyle | ||
className?: string | ||
onChangeText?: (text: string) => void | ||
value: string | number | ||
formatter?: Intl.NumberFormat | ||
autoFocus?: boolean | ||
suffix?: string | ||
suffixClassName?: string | ||
} | ||
|
||
export function TextTicker({ | ||
style, | ||
className, | ||
value = '0', | ||
formatter = new Intl.NumberFormat('en-US'), | ||
suffix, | ||
suffixClassName, | ||
}: TextTickerProps) { | ||
const initialFontSize = style?.fontSize ?? 68 | ||
const animationDuration = 300 | ||
const [fontSize, setFontSize] = useState(initialFontSize) | ||
|
||
const formattedNumbers = React.useMemo(() => { | ||
return formatNumberWithCommas(formatter, parseFloat(String(value) || '0')) | ||
}, [value, formatter]) | ||
|
||
return ( | ||
<Animated.View | ||
style={{ | ||
height: fontSize * 1.2, | ||
}} | ||
className="w-full" | ||
> | ||
{/* Using a dummy Text to let React Native do the math for the font size, | ||
in case the text will not fit on a single line. */} | ||
<Text | ||
numberOfLines={1} | ||
adjustsFontSizeToFit | ||
className={cn( | ||
className, | ||
'absolute text-center left-0 right-0 opacity-0', | ||
)} | ||
style={{ | ||
fontSize: initialFontSize, | ||
lineHeight: initialFontSize, | ||
top: -10000, | ||
}} | ||
onTextLayout={(e) => { | ||
setFontSize(Math.round(e.nativeEvent.lines[0].ascender)) | ||
}} | ||
> | ||
{formattedNumbers.map((x) => x.value).join('')} | ||
{suffix} | ||
</Text> | ||
<Animated.View className="flex-row items-end justify-center w-full flex-1 overflow-hidden"> | ||
{formattedNumbers.map((formattedNumber) => { | ||
return ( | ||
<Animated.View | ||
layout={LinearTransition.duration(animationDuration)} | ||
key={formattedNumber.key} | ||
entering={SlideInDown.duration( | ||
animationDuration, | ||
).withInitialValues({ | ||
originY: initialFontSize / 2, | ||
})} | ||
exiting={SlideOutDown.duration( | ||
animationDuration, | ||
).withInitialValues({ | ||
transform: [{ translateY: -initialFontSize / 2 }], | ||
})} | ||
> | ||
<Animated.Text | ||
style={[style, { fontSize }]} | ||
className={className} | ||
> | ||
{formattedNumber.value} | ||
</Animated.Text> | ||
</Animated.View> | ||
) | ||
})} | ||
{!!suffix && ( | ||
<Animated.View | ||
layout={LinearTransition.duration(animationDuration)} | ||
style={{ marginBottom: fontSize / 6 }} | ||
> | ||
<Animated.Text | ||
style={{ fontSize: fontSize / 3 }} | ||
className={suffixClassName} | ||
> | ||
{suffix} | ||
</Animated.Text> | ||
</Animated.View> | ||
)} | ||
</Animated.View> | ||
</Animated.View> | ||
) | ||
} |