Skip to content

Commit 0e701da

Browse files
authored
support timer component (#943)
1 parent 34279b9 commit 0e701da

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

example/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ import PinInputExample from "./PinInputExample";
7474
import KeyboardAvoidingViewExample from "./KeyboardAvoidingViewExample";
7575
import ThemeExample from "./ThemeExample";
7676
import LoadingIndicatorExample from "./LoadingIndicatorExample";
77+
import TimerExample from "./TimerExample";
7778

7879
const ROUTES = {
80+
Timer: TimerExample,
7981
LoadingIndicator: LoadingIndicatorExample,
8082
Theme: ThemeExample,
8183
AudioPlayer: AudioPlayerExample,

example/src/TimerExample.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as React from "react";
2+
import { View, StyleSheet, Text } from "react-native";
3+
import { withTheme } from "@draftbit/ui";
4+
import Section, { Container } from "./Section";
5+
import { Button, Timer } from "@draftbit/core";
6+
7+
const TimerExample: React.FC = () => {
8+
const timerRef = React.useRef<any>(null);
9+
const [countDirection, setCountDirection] = React.useState<"up" | "down">(
10+
"up"
11+
);
12+
13+
const handleStart = () => timerRef.current?.start();
14+
const handleStop = () => timerRef.current?.stop();
15+
const handleReset = () => timerRef.current?.reset();
16+
const handleResetToCustomTime = () => timerRef.current?.reset(5000);
17+
18+
const handleDirectionToggle = () =>
19+
setCountDirection((prev) => (prev === "up" ? "down" : "up"));
20+
return (
21+
<Container style={{}}>
22+
<Section style={{}} title="Default">
23+
<View
24+
style={{
25+
flexDirection: "column",
26+
justifyContent: "center",
27+
}}
28+
>
29+
<Timer
30+
ref={timerRef}
31+
initialTime={60000}
32+
timerEndTime={70000}
33+
updateInterval={1000}
34+
format="mm:ss"
35+
countDirection={countDirection}
36+
onTimerChange={(value: number) => {
37+
console.log("onTimerChange : ", value);
38+
}}
39+
onTimerEnd={() => {
40+
console.log("onTimerEnd");
41+
// eslint-disable-next-line no-alert
42+
alert("onTimerEnd");
43+
}}
44+
style={{
45+
fontSize: 50,
46+
fontWeight: "bold",
47+
}}
48+
/>
49+
<Text style={styles.directionText}>
50+
Count direction : {countDirection}
51+
</Text>
52+
<View style={styles.buttonsContainer}>
53+
<Button title="Start Timer" onPress={handleStart} />
54+
<Button title="Stop Timer" onPress={handleStop} />
55+
<Button title="Reset Timer" onPress={handleReset} />
56+
<Button title="Reset to 5s" onPress={handleResetToCustomTime} />
57+
<Button
58+
title={`Toggle Direction`}
59+
onPress={handleDirectionToggle}
60+
/>
61+
</View>
62+
</View>
63+
</Section>
64+
</Container>
65+
);
66+
};
67+
68+
const styles = StyleSheet.create({
69+
container: {
70+
flex: 1,
71+
justifyContent: "center",
72+
padding: 20,
73+
},
74+
buttonsContainer: {
75+
marginTop: 20,
76+
gap: 10,
77+
},
78+
directionText: {
79+
textAlign: "center",
80+
fontSize: 20,
81+
marginVertical: 15,
82+
},
83+
});
84+
85+
export default withTheme(TimerExample);
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, {
2+
useState,
3+
useEffect,
4+
useRef,
5+
useImperativeHandle,
6+
forwardRef,
7+
} from "react";
8+
import { Text, StyleSheet, TextStyle, StyleProp } from "react-native";
9+
10+
interface TimerProps {
11+
style?: StyleProp<TextStyle>;
12+
initialTime?: number;
13+
updateInterval?: number;
14+
format?: "ss" | "mm:ss" | "hh:mm:ss" | "ss:ms" | "mm:ss:ms" | "hh:mm:ss:ms";
15+
onTimerChange?: (time: number) => void;
16+
onTimerEnd?: () => void;
17+
countDirection?: "up" | "down";
18+
timerEndTime?: number;
19+
}
20+
21+
export interface TimerHandle {
22+
start: () => void;
23+
stop: () => void;
24+
reset: (newTime?: number) => void;
25+
}
26+
27+
const Timer = forwardRef<TimerHandle, TimerProps>(
28+
(
29+
{
30+
style,
31+
initialTime,
32+
updateInterval = 1000,
33+
format = "mm:ss",
34+
onTimerChange,
35+
onTimerEnd,
36+
countDirection = "up",
37+
timerEndTime,
38+
},
39+
ref
40+
) => {
41+
const defaultInitialTime = countDirection === "up" ? 0 : 100000;
42+
const [time, setTime] = useState(initialTime ?? defaultInitialTime);
43+
const timerRef = useRef<NodeJS.Timeout | null>(null);
44+
45+
useEffect(() => {
46+
onTimerChange?.(time);
47+
// eslint-disable-next-line react-hooks/exhaustive-deps
48+
}, [time]);
49+
50+
useImperativeHandle(ref, () => ({
51+
start: startTimer,
52+
stop: stopTimer,
53+
reset: resetTimer,
54+
}));
55+
56+
const startTimer = () => {
57+
if (timerRef.current) return;
58+
timerRef.current = setInterval(() => {
59+
setTime((prevTime) => {
60+
const newTime =
61+
countDirection === "up"
62+
? prevTime + updateInterval
63+
: prevTime - updateInterval;
64+
// Count down
65+
if (newTime <= 0 && countDirection === "down") {
66+
clearTimer();
67+
onTimerEnd?.();
68+
return 0;
69+
}
70+
// Count up
71+
if (
72+
countDirection === "up" &&
73+
timerEndTime !== undefined &&
74+
newTime >= timerEndTime
75+
) {
76+
clearTimer();
77+
onTimerEnd?.();
78+
return timerEndTime;
79+
}
80+
81+
return newTime;
82+
});
83+
}, updateInterval);
84+
};
85+
86+
const stopTimer = () => clearTimer();
87+
88+
const resetTimer = (
89+
newTime: number = initialTime ?? defaultInitialTime
90+
) => {
91+
clearTimer();
92+
setTime(newTime);
93+
};
94+
95+
const clearTimer = () => {
96+
if (timerRef.current) {
97+
clearInterval(timerRef.current);
98+
timerRef.current = null;
99+
}
100+
};
101+
102+
useEffect(() => {
103+
return () => clearTimer();
104+
}, []);
105+
106+
const formatTime = (milliseconds: number): string => {
107+
const totalSeconds = Math.floor(milliseconds / 1000);
108+
const hours = Math.floor(totalSeconds / 3600);
109+
const minutes = Math.floor((totalSeconds - hours * 3600) / 60);
110+
const seconds = totalSeconds - hours * 3600 - minutes * 60;
111+
const ms = milliseconds % 1000;
112+
113+
const formattedHours = String(hours).padStart(2, "0");
114+
const formattedMinutes = String(minutes).padStart(2, "0");
115+
const formattedSeconds = String(seconds).padStart(2, "0");
116+
const formattedMs = String(ms).padStart(3, "0");
117+
118+
return format
119+
.replace("hh", formattedHours)
120+
.replace("mm", formattedMinutes)
121+
.replace("ss", formattedSeconds)
122+
.replace("ms", formattedMs);
123+
};
124+
125+
return (
126+
<Text style={[styles.defaultTimerStyle, style]}>{formatTime(time)}</Text>
127+
);
128+
}
129+
);
130+
131+
const styles = StyleSheet.create({
132+
defaultTimerStyle: {
133+
fontSize: 24,
134+
textAlign: "center",
135+
},
136+
});
137+
138+
export default Timer;

packages/core/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export { default as SimpleStyleScrollView } from "./components/SimpleStyleScroll
7676
export { default as SimpleStyleSectionList } from "./components/SimpleStyleScrollables/SimpleStyleSectionList";
7777
export { default as SimpleStyleSwipeableList } from "./components/SimpleStyleScrollables/SimpleStyleSwipeableList";
7878
export { default as LoadingIndicator } from "./components/LoadingIndicator";
79+
export { default as Timer } from "./components/Timer";
7980

8081
/* Deprecated: Fix or Delete! */
8182
export { default as AccordionItem } from "./deprecated-components/AccordionItem";

packages/ui/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export {
7474
SimpleStyleSectionList,
7575
SimpleStyleSwipeableList,
7676
LoadingIndicator,
77+
Timer,
7778
} from "@draftbit/core";
7879

7980
export {

0 commit comments

Comments
 (0)