Skip to content

Commit 5f4643e

Browse files
committed
feat: add countdown component
1 parent 44b7f25 commit 5f4643e

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

share/components/CountDown.tsx

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import React, {useCallback, useEffect, useRef, useState} from 'react'
2+
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
3+
import dayjs from 'dayjs'
4+
import {colors} from '../themes'
5+
import {Text} from 'rn-base-component'
6+
7+
export interface Props {
8+
style?: StyleProp<ViewStyle>
9+
digitStyle?: StyleProp<ViewStyle>
10+
digitTxtStyle?: StyleProp<TextStyle>
11+
timeToShow?: string[]
12+
separatorStyle?: StyleProp<TextStyle>
13+
showSeparator?: boolean
14+
until?: number
15+
size?: number
16+
running?: boolean
17+
onFinish?: () => void
18+
countDownTo?: dayjs.Dayjs
19+
}
20+
21+
export const CountDown: React.FC<Props> = ({
22+
style,
23+
until = 0,
24+
size = 14,
25+
timeToShow = ['D', 'H', 'M', 'S'],
26+
separatorStyle,
27+
showSeparator = true,
28+
digitStyle,
29+
digitTxtStyle,
30+
running = true,
31+
onFinish,
32+
countDownTo,
33+
}) => {
34+
const timerRef = useRef<NodeJS.Timeout | null>(null)
35+
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
36+
const [countDown, setCountDown] = useState(Math.max(until, 0))
37+
38+
const updateTimer = useCallback(() => {
39+
if (!running) {
40+
return
41+
}
42+
if (countDown === 1) {
43+
if (onFinish) {
44+
onFinish()
45+
}
46+
}
47+
48+
if (countDown === 0) {
49+
setCountDown(0)
50+
} else {
51+
setCountDown(Math.max(0, countDown - 1))
52+
}
53+
}, [countDown, onFinish, running])
54+
55+
const updateCountDownTimer = useCallback(() => {
56+
if (countDownTo) {
57+
const count = Math.max(0, countDownTo.diff(dayjs(), 'seconds'))
58+
setCountDown(count)
59+
if (timeoutRef.current) {
60+
clearTimeout(timeoutRef.current)
61+
}
62+
if (count > 0) {
63+
// Get current millisecond to calculate time left for next seconds change
64+
const milliSecond = 1000 - dayjs().millisecond()
65+
timeoutRef.current = setTimeout(() => {
66+
updateCountDownTimer()
67+
}, milliSecond)
68+
}
69+
}
70+
}, [countDownTo])
71+
72+
useEffect(() => {
73+
if (countDownTo) {
74+
updateCountDownTimer()
75+
} else {
76+
timerRef.current = setInterval(updateTimer, 1000)
77+
}
78+
79+
return () => {
80+
if (timerRef.current) {
81+
clearInterval(timerRef.current)
82+
}
83+
if (timeoutRef.current) {
84+
clearTimeout(timeoutRef.current)
85+
}
86+
}
87+
}, [countDownTo, updateCountDownTimer, updateTimer])
88+
89+
const getTimeLeft = useCallback(() => {
90+
const padStart = (n: number) => (timeToShow.length > 1 && n < 10 ? '0' : '') + n
91+
return {
92+
seconds: padStart(countDown % 60),
93+
minutes: padStart(parseInt(`${countDown / 60}`, 10) % 60),
94+
hours: padStart(parseInt(`${countDown / (60 * 60)}`, 10) % 24),
95+
days: padStart(parseInt(`${countDown / (60 * 60 * 24)}`, 10)),
96+
}
97+
}, [countDown, timeToShow.length])
98+
99+
const renderDigit = useCallback(
100+
(d: string) => (
101+
<Text color={colors.gray500} style={[{fontSize: size}, digitTxtStyle]}>
102+
{timeToShow.length === 1 ? `(${d}s)` : d}
103+
</Text>
104+
),
105+
[digitStyle, digitTxtStyle, size, timeToShow],
106+
)
107+
108+
const renderDoubleDigits = (digits: string) => (
109+
<View style={styles.doubleDigitCont}>
110+
<View style={styles.timeInnerCont}>{renderDigit(digits)}</View>
111+
</View>
112+
)
113+
114+
const renderSeparator = useCallback(() => <Text style={separatorStyle}>:</Text>, [separatorStyle, size])
115+
116+
const renderCountDown = () => {
117+
const {days, hours, minutes, seconds} = getTimeLeft()
118+
119+
return (
120+
<View style={styles.timeCont}>
121+
{timeToShow.includes('D') && renderDoubleDigits(days.toString())}
122+
{showSeparator && timeToShow.includes('D') && timeToShow.includes('H') ? renderSeparator() : null}
123+
{timeToShow.includes('H') && renderDoubleDigits(hours.toString())}
124+
{showSeparator && timeToShow.includes('H') && timeToShow.includes('M') ? renderSeparator() : null}
125+
{timeToShow.includes('M') && renderDoubleDigits(minutes.toString())}
126+
{showSeparator && timeToShow.includes('M') && timeToShow.includes('S') && renderSeparator()}
127+
{timeToShow.includes('S') && renderDoubleDigits(seconds.toString())}
128+
</View>
129+
)
130+
}
131+
132+
return <View style={style}>{renderCountDown()}</View>
133+
}
134+
135+
const styles = StyleSheet.create({
136+
timeCont: {
137+
flexDirection: 'row',
138+
justifyContent: 'center',
139+
},
140+
timeTxt: {
141+
backgroundColor: 'transparent',
142+
},
143+
timeInnerCont: {
144+
flexDirection: 'row',
145+
justifyContent: 'center',
146+
alignItems: 'center',
147+
},
148+
doubleDigitCont: {
149+
justifyContent: 'center',
150+
alignItems: 'center',
151+
},
152+
separatorContainer: {justifyContent: 'center', alignItems: 'center'},
153+
})

0 commit comments

Comments
 (0)