Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: countdown component #74

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions example/.ondevice/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const getStories = () => {
"./src/stories/Card.stories.tsx": require("../src/stories/Card.stories.tsx"),
"./src/stories/Checkbox.stories.tsx": require("../src/stories/Checkbox.stories.tsx"),
"./src/stories/CodeInput.stories.tsx": require("../src/stories/CodeInput.stories.tsx"),
"./src/stories/CountDown.stories.tsx": require("../src/stories/CountDown.stories.tsx"),
"./src/stories/Progress.stories.tsx": require("../src/stories/Progress.stories.tsx"),
"./src/stories/RadioButton.stories.tsx": require("../src/stories/RadioButton.stories.tsx"),
"./src/stories/Slider.stories.tsx": require("../src/stories/Slider.stories.tsx"),
Expand Down
43 changes: 43 additions & 0 deletions example/src/stories/CountDown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import type {ComponentMeta, ComponentStory} from '@storybook/react'
import {theme, CountDown} from 'rn-base-component'
import {ScrollView, StyleSheet, View} from 'react-native'
import {metrics} from '../../../src/helpers'

export default {
title: 'components/CountDown',
component: CountDown,
} as ComponentMeta<typeof CountDown>

export const CountDownComponent: ComponentStory<typeof CountDown> = args => (
<ScrollView showsVerticalScrollIndicator={false}>
<View style={styles.marginBottom} />
<CountDown {...args} />
</ScrollView>
)
CountDownComponent.args = {
initialSeconds: 10,
loop: false,
format: 'mmss',
}

const styles = StyleSheet.create({
spacingTop: {
marginVertical: metrics.borderRadiusLarge,
},
background: {
backgroundColor: '#e6e0ec',
},
marginBottom: {
marginBottom: metrics.xxl,
},
CountDownContainer: {
marginHorizontal: metrics.medium,
},
inputContainer: {
borderWidth: theme?.borderWidths.tiny,
borderRadius: metrics.borderRadius,
paddingHorizontal: metrics.xs,
height: metrics.xxxl,
},
})
194 changes: 194 additions & 0 deletions src/components/CountDown/CountDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, {useEffect, useRef, useState} from 'react'
import {AppState, type StyleProp, type ViewStyle, type TextStyle} from 'react-native'
import styled from 'styled-components/native'
import {Text} from '../Text/Text'
import {useTheme} from '../../hooks'

export const FormatTime = {
mmss: 'mm:ss',
hhmmss: 'HH:mm:ss',
ddhhmmss: 'DD:HH:mm:ss',
} as const


export type CountDownProps = {
ZanBin marked this conversation as resolved.
Show resolved Hide resolved
ZanBin marked this conversation as resolved.
Show resolved Hide resolved
/*
init time countdown by second
*/
initialSeconds: number
/*
call when count down finish
*/
onFinish?: () => void
/*
container countdown view style
*/
containerStyle?: StyleProp<ViewStyle>
/*
text countdown style
*/
textStyle?: StyleProp<TextStyle>
/*
element countdown style
*/
elementStyle?: StyleProp<ViewStyle>
/*
colons countdown
*/
colonsStyle?: StyleProp<TextStyle>
/*
loop countdown
*/
loop?: boolean
/*
format countdown as key
*/
format?: keyof typeof FormatTime
/*
default count time after milisecond
*/
intervalTimeBySecond?: number
}
const milisecond = 1000
const numberSecondPerMinute = 60
const secondPerHour = 3600
const hourPerDay = 24
const secondPerDay = 86400

export const CountDown: React.FunctionComponent<CountDownProps> = ({
initialSeconds = 300,
containerStyle,
onFinish,
textStyle,
loop,
format = 'mmss',
intervalTimeBySecond = 1000,
elementStyle,
colonsStyle,
}) => {
const CountDownTheme = useTheme().components.CountDown

const [seconds, setSeconds] = useState(initialSeconds)
const appState = useRef(AppState.currentState)
const timeEnd = useRef(new Date().getTime() + initialSeconds * milisecond)
const [appStateVisible, setAppStateVisible] = useState(appState.current)

/*
check app state is change
JS don't run setTimeout in background
so we need to check appstate to make sure countdown is correct
*/
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
appState.current = nextAppState
setAppStateVisible(appState.current)
})
return () => {
subscription.remove()
}
}, [])

useEffect(() => {
const timeLoop = intervalTimeBySecond - new Date().getMilliseconds()
const timeout = setTimeout(() => {
if (seconds >= 0) {
const uff = timeEnd.current - new Date().getTime()
setSeconds(uff / milisecond)
}
if (seconds < 1) {
if (loop) {
timeEnd.current = timeEnd.current - new Date().getTime()
setSeconds(initialSeconds)
} else {
clearTimeout(timeout)
if (onFinish) {
onFinish()
}
}
}
}, timeLoop)
return () => {
clearTimeout(timeout)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [seconds, appStateVisible])
ZanBin marked this conversation as resolved.
Show resolved Hide resolved

const renderColons = () => (
<Container>
<Label style={[CountDownTheme.textStyle, textStyle, colonsStyle]}>:</Label>
</Container>
)

const renderTimer = () => {
/*
caculate minutes
*/
const resultCaculatMinute = Math.floor(seconds / numberSecondPerMinute) % numberSecondPerMinute
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong typo

Suggested change
const resultCaculatMinute = Math.floor(seconds / numberSecondPerMinute) % numberSecondPerMinute
const resultCaculateMinute = Math.floor(seconds / numberSecondPerMinute) % numberSecondPerMinute

const minute = resultCaculatMinute >= 0 ? resultCaculatMinute : 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it need to be >= 0 ? shouldn't it be only > 0

if resultCaculateMinute = 0, it will be same as the value return from else condition

const textMinute = (
<Container style={elementStyle}>
<Label style={[CountDownTheme.textStyle, textStyle]}>{minute.toString().padStart(2, '0')}</Label>
</Container>
)
/*
caculate seconds
*/
const resultCaculatSecond = Math.round(seconds % numberSecondPerMinute)
const second = resultCaculatSecond >= 0 ? resultCaculatSecond : 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with const minute

const textSecond = (
<Container style={elementStyle}>
<Label style={[CountDownTheme.textStyle, textStyle]}>{second.toString().padStart(2, '0')}</Label>
</Container>
)

let textDay: React.ReactNode | null = null
let textHour: React.ReactNode | null = null
/*
caculate day
*/
if (FormatTime.ddhhmmss === FormatTime[format]) {
const resultCaculatDay = Math.floor(seconds / secondPerDay)
const day = resultCaculatDay >= 0 ? resultCaculatDay : 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

textDay = (
<Container style={elementStyle}>
<Label style={[CountDownTheme.textStyle, textStyle]}>{day.toString().padStart(2, '0')}</Label>
</Container>
)
}
/*
caculate hours
*/
if (FormatTime.hhmmss === FormatTime[format] || FormatTime.ddhhmmss === FormatTime[format]) {
const resultCaculatHour = Math.floor(seconds / secondPerHour)
const hour = resultCaculatHour >= 0 ? resultCaculatHour % hourPerDay : 0
textHour = (
<Container style={elementStyle}>
<Label style={[CountDownTheme.textStyle, textStyle]}>{hour.toString().padStart(2, '0')}</Label>
</Container>
)
}

return (
<Container style={[CountDownTheme.containerStyle, containerStyle]}>
{textDay && textDay}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it convert to Boolean, because textDay has case = null, so if it return null it will return error string outside Text component

{textDay && renderColons()}
{textHour && textHour}
{textHour && renderColons()}
{textMinute}
{renderColons()}
{textSecond}
</Container>
)
}

return renderTimer()
}

const Container = styled.View({
ZanBin marked this conversation as resolved.
Show resolved Hide resolved
flexDirection: 'row',
})
ZanBin marked this conversation as resolved.
Show resolved Hide resolved

const Label = styled(Text)(({theme}) => ({
color: theme?.colors?.black,
fontSize: theme?.fontSizes?.xl,
}))
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {CodeInput} from './CodeInput/CodeInput'
import Slider from './Slider/Slider'
import Card from './Card/Card'
import {TextInput} from './TextInput/TextInput'
import {CountDown} from './CountDown/CountDown'

export {
Button,
Expand All @@ -23,5 +24,6 @@ export {
Card,
TextInput,
Accordion,
CountDown,
}
export * from './Text/Text'
12 changes: 12 additions & 0 deletions src/theme/components/Countdown/Countdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import base from '../../base'
import {metrics} from '../../../helpers'
import type {CountDownProps} from '../../../components/CountDown/CountDown'

export type CountDownThemeProps = Pick<CountDownProps, 'textStyle'>

export const CountDownTheme: CountDownThemeProps = {
textStyle: {
color: base.colors.black,
fontSize: metrics.sMedium,
},
}
2 changes: 2 additions & 0 deletions src/theme/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ButtonSecondaryTheme,
ButtonTransparentTheme,
} from './Button'
import {CountDownTheme} from './Countdown/Countdown'
import {CheckboxTheme} from './Checkbox'
import {TextTheme} from './Text'

Expand All @@ -16,4 +17,5 @@ export default {
ButtonTransparent: ButtonTransparentTheme,
Checkbox: CheckboxTheme,
Text: TextTheme,
CountDown: CountDownTheme,
}
Loading