Skip to content
This repository was archived by the owner on Nov 10, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import StopWatch from './src/components/StopWatch';

export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
<StopWatch />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: "#0D0916",
alignItems: "center",
justifyContent: "center",
},
});
8 changes: 0 additions & 8 deletions src/StopWatch.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions src/StopWatchButton.tsx

This file was deleted.

194 changes: 194 additions & 0 deletions src/components/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { FlatList, StyleSheet, Text, View } from 'react-native';
import StopWatchButton from './StopWatchButton';
import { useEffect, useState } from 'react';
import { formatTime } from '../utils/formatTime';
import { LapItem } from '../types/LapItem';

//renderLap function is used to render each lapItem into a row with the formatted time
const renderLap = ({ item }: { item: LapItem }) => {
return (
<View style={styles.lapItemContainer}>
<View style={styles.lapsHeaderLeftSection}>
<Text style={styles.text}>
{item.index.toString()}
</Text>
</View>
<View style={styles.lapsHeaderMiddleSection}>
<Text style={styles.text}>
{formatTime(item.lapTime)}
</Text>
</View>
<View style={styles.lapsHeaderRightSection}>
<Text style={styles.text}>
{formatTime(item.generalTime)}
</Text>
</View>
</View>

);
};

export default function StopWatch() {
//States:
//startTime, the startTime of the stopwatch, necessary for precise milliseconds display
//time, the time in milliseconds passed from the beginning
//isTimerOn, a boolean state needed to set the timer on/off
//lapItems, a state array needed to add lapItems and display the laps
const [startTime, setStartTime] = useState<number | null>(null);
const [time, setTime] = useState(0);
const [isTimerOn, setIsTimerOn] = useState(false);
const [lapItems, setLapItems] = useState<LapItem[]>([]);

//useEffect is needed here to update the time once the user presses Start or Stop
useEffect(() => {
let interval: number | null = null;

//Checks if the timer is on:
//If it is on: keep incrementing the time
if(isTimerOn){
if (startTime === null) {
setStartTime(Date.now() - time);
}

interval = setInterval(() => {
setTime(Date.now() - startTime!);
}, 1);
} else {
//If the timer is off: stop incrementing the time
setStartTime(null);
}

//Clear the interval on return
return () => clearInterval(interval!);
})

//reset the timer by resetting all time states and laps
function resetTimer(){
setTime(0);
setIsTimerOn(false);
setLapItems([]);
}

//function to add a lap item
function addLap(){
//Gets the latest lap item to get the time and the index, if there's no previous lap item, start at 0
const newestTime = lapItems.length > 0 ? lapItems[lapItems.length-1].generalTime : 0;
const newestIndex = lapItems.length > 0 ? lapItems[lapItems.length-1].index : 0;

//Create a new lap item using the latest lap item
//Increment the index and calculate the lap time using the time now - the time at which the previous lap was at
const newLapItem = {
index: newestIndex+1,
lapTime: time-newestTime,
generalTime: time
}

//Create a new laps array and add the new object
const newLaps = [
...lapItems.slice(0, lapItems.length),
newLapItem,
...lapItems.slice(lapItems.length)
];

//Set the lap items to the new array
setLapItems(newLaps);
}

return (
<View style={styles.container}>
<View style={styles.stopwatchContainer}>
<Text style={styles.stopwatchText}>{formatTime(time)}</Text>
</View>
<View style={styles.lapsContainer}>
{lapItems.length > 0 ?
<View style={styles.lapsHeader}>
<View style={styles.lapsHeaderLeftSection}>
<Text style={styles.text}>Lap</Text>
</View>
<View style={styles.lapsHeaderMiddleSection}>
<Text style={styles.text}>Lap Time</Text>
</View>
<View style={styles.lapsHeaderRightSection}>
<Text style={styles.text}>Overall Time</Text>
</View>
</View> :
null}
<FlatList
data={[...lapItems].reverse()}
renderItem={renderLap}
scrollEnabled={true}
showsVerticalScrollIndicator={false}
/>
</View>
<View style={styles.buttonsContainer}>
<View style={styles.buttonsRowContainer}>
<StopWatchButton action='start' onPress={() => setIsTimerOn(true)} isDisabled={isTimerOn} buttonColor='#62a84f' buttonBorderColor='#326345'/>
<StopWatchButton action='stop' onPress={() => setIsTimerOn(false)} isDisabled={!isTimerOn}></StopWatchButton>
</View>
<View style={styles.buttonsRowContainer}>
<StopWatchButton action='lap' onPress={addLap} isDisabled={!isTimerOn}></StopWatchButton>
<StopWatchButton action='reset' onPress={resetTimer} isDisabled={time === 0} buttonColor='#e3494c' buttonBorderColor='#940628'></StopWatchButton>
</View>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
},
stopwatchContainer: {
justifyContent: "flex-end",
alignItems: "center",
height: "30%",
width: "90%",
},
lapsHeader: {
flexDirection: "row",
width: "100%",
marginBottom: 4,
},
lapsHeaderLeftSection: {
width: "20%",
},
lapsHeaderMiddleSection: {
width: "40%",
alignItems: "center",
},
lapsHeaderRightSection: {
width: "40%",
alignItems: "flex-end",
},
lapsContainer: {
height: "40%",
width: "80%",
},
lapItemContainer:{
flexDirection: "row",
alignItems: "center",
},
buttonsContainer:{
justifyContent: "flex-end",
alignItems: "center",
height: "30%",
width: "90%",
},
buttonsRowContainer: {
flexDirection: "row",
justifyContent: "space-between",
width: "80%",
marginBottom: 32,
},
stopwatchText: {
color: "#FAFAFA",
fontSize: 48,
},
text: {
color: "#FAFAFA",
fontSize: 16,
},
})
66 changes: 66 additions & 0 deletions src/components/StopWatchButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StopWatchButtonProps } from '../types/ButtonProps';

export default function StopWatchButton(
{
action,
onPress,
height = 40,
width = 80,
buttonColor = "#6BBDF3",
buttonBorderColor = "#4796D0",
borderRadius = 8,
isDisabled = false,
hitslop = { top: 20, bottom: 20, left: 20, right: 20 },
...props
}
: StopWatchButtonProps) {

//Button title is just the action name capitalized
const buttonTitle = action.substring(0, 1).toUpperCase() + action.substring(1);

return (
<View
style={{
height: height,
width: width,
}}
>
<View
style={{
backgroundColor: isDisabled ? "#484254" : buttonColor,
borderColor: isDisabled ? "#777777" : buttonBorderColor,
borderWidth: 1,
borderRadius: borderRadius,
}}
{...props}
>
<TouchableOpacity
hitSlop={hitslop}
disabled={isDisabled}
style={styles.button}
onPress={!isDisabled ? onPress : () => null}
>
<Text style={styles.text}>{buttonTitle}</Text>
</TouchableOpacity>
</View>

</View>
);
}

const styles = StyleSheet.create({
container: {
width: "100%",
height: "100%",
},
button: {
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center"
},
text: {
color: "#FAFAFA",
},
});
28 changes: 28 additions & 0 deletions src/types/ButtonProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DimensionValue, Insets } from "react-native";
import { StopwatchButtonActionTypes } from "./StopwatchButtonActionTypes";

//The stopwatch button takes in as input the following:
//action: the type of the button, what it does
//onPress: the function which is executes on press
//height: adjustable height if needed
//width: adjustable width if needed
//buttonColor: adjustable button color if needed, default is light blue
//buttonBorderColor: adjustable border color if needed, default is shade of light blue
//isDisabled: boolean to disable the button if needed
//hitslop: adjustable hitslop, default is 20
export interface StopWatchButtonProps {
action: StopwatchButtonActionTypes
onPress(): void;

height?: DimensionValue;
width?: DimensionValue;

buttonColor?: string;
buttonBorderColor?: string;

borderRadius?: number;
bottomBorderWidth?: number;

isDisabled?: boolean;
hitslop?: Insets;
}
6 changes: 6 additions & 0 deletions src/types/LapItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//LapItem contains the field needed to display the laps
export type LapItem = {
index: number;
lapTime: number;
generalTime: number;
}
6 changes: 6 additions & 0 deletions src/types/StopwatchButtonActionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//The different types of buttons
export type StopwatchButtonActionTypes =
| "start"
| "stop"
| "reset"
| "lap";
9 changes: 9 additions & 0 deletions src/utils/formatTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//Function which formats the time from milliseconds to HH:MM:SS.(mls)
export function formatTime(millis: number): string {
const hours = Math.floor((millis/1000)/3600);
const minutes = Math.floor(((millis/1000) - (hours*3600))/60);
const seconds = Math.floor((millis/1000)%60);
const millisFormatted = millis%1000;

return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${millisFormatted.toString().padStart(3, "0")}`;
}
2 changes: 1 addition & 1 deletion tests/Stopwatch.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Stopwatch from '../src/Stopwatch';
import Stopwatch from '../src/components/StopWatch';

describe('Stopwatch', () => {
test('renders initial state correctly', () => {
Expand Down