diff --git a/App.tsx b/App.tsx
index 0329d0c..276e2b9 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,10 +1,10 @@
-import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, Text, View } from 'react-native';
-
+import { StatusBar } from "expo-status-bar";
+import { StyleSheet, Text, View } from "react-native";
+import StopWatch from "./src/StopWatch";
export default function App() {
return (
- Open up App.tsx to start working on your app!
+
);
@@ -13,8 +13,6 @@ export default function App() {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#fff',
- alignItems: 'center',
- justifyContent: 'center',
+ backgroundColor: "#fff",
},
});
diff --git a/package-lock.json b/package-lock.json
index ea36168..bcc9dd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,9 @@
"@testing-library/react-native": "^12.4.3",
"@tsconfig/react-native": "^3.0.2",
"@types/jest": "^29.5.11",
+ "@types/node": "^20.11.6",
"@types/react": "~18.2.14",
+ "@types/react-native": "^0.73.0",
"@types/react-test-renderer": "^18.0.7",
"typescript": "^5.3.3"
}
@@ -7098,9 +7100,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.10.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
- "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
+ "version": "20.11.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz",
+ "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -7122,6 +7124,16 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/react-native": {
+ "version": "0.73.0",
+ "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.73.0.tgz",
+ "integrity": "sha512-6ZRPQrYM72qYKGWidEttRe6M5DZBEV5F+MHMHqd4TTYx0tfkcdrUFGdef6CCxY0jXU7wldvd/zA/b0A/kTeJmA==",
+ "deprecated": "This is a stub types definition. react-native provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "react-native": "*"
+ }
+ },
"node_modules/@types/react-test-renderer": {
"version": "18.0.7",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.7.tgz",
diff --git a/package.json b/package.json
index bf0f5a3..de1eafd 100644
--- a/package.json
+++ b/package.json
@@ -12,17 +12,19 @@
"dependencies": {
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
- "react": "18.2.0",
- "react-native": "0.72.6",
+ "jest": "^29.2.1",
"jest-expo": "~49.0.0",
- "jest": "^29.2.1"
+ "react": "18.2.0",
+ "react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@testing-library/react-native": "^12.4.3",
"@tsconfig/react-native": "^3.0.2",
"@types/jest": "^29.5.11",
+ "@types/node": "^20.11.6",
"@types/react": "~18.2.14",
+ "@types/react-native": "^0.73.0",
"@types/react-test-renderer": "^18.0.7",
"typescript": "^5.3.3"
},
diff --git a/src/Components/LapsList.tsx b/src/Components/LapsList.tsx
new file mode 100644
index 0000000..01f241f
--- /dev/null
+++ b/src/Components/LapsList.tsx
@@ -0,0 +1,80 @@
+import { ScrollView, StyleSheet, Text, View } from "react-native";
+// Import utility function for time display
+import { displayTime } from "../util/displayTime";
+
+// Define TypeScript interface for LapsProps
+interface LapsProps {
+ laps: number[] | null; // Array of lap times or null
+}
+
+// Define TypeScript interface for LapsEntryProps
+interface LapsEntryProps {
+ time: number; // Time for each lap
+ idx: number; // Index of the lap
+}
+
+// Functional component to display each lap entry
+const LapsEntry = ({ time, idx }: LapsEntryProps) => {
+ return (
+
+ {/* Display the lap number and time */}
+ {`Lap #${idx + 1}: ${displayTime(
+ time
+ )}`}
+ {/* Separator line */}
+
+
+ );
+};
+
+// Functional component to display the list of laps
+export const LapsList = ({ laps }: LapsProps) => {
+ // Using ScrollView for the list of laps
+ return (
+
+ {/* Title for the laps section */}
+ Laps
+ {/* Check if laps exist and render them */}
+ {laps && (
+
+
+ {/* Map through each lap and render a LapsEntry component */}
+ {laps.map((time, idx) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ lapsListContainer: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ lapsEntry: {
+ padding: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: "#ccc",
+ fontSize: 16,
+ color: "#333",
+ width: "100%",
+ },
+ lapsListContent: { paddingBottom: 50 },
+ lapsList: {
+ paddingHorizontal: 30,
+ width: "100%",
+ },
+ title: {
+ fontSize: 30,
+ fontWeight: "bold",
+ color: "#333",
+ marginBottom: 10,
+ },
+});
diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx
index 5c7eb74..6033459 100644
--- a/src/StopWatch.tsx
+++ b/src/StopWatch.tsx
@@ -1,8 +1,146 @@
-import { View } from 'react-native';
+// Import necessary components from react-native and react
+import { View, StyleSheet, Text } from "react-native";
+import { useState, useEffect } from "react";
+// Import custom components and utility functions
+import StopWatchButton from "./StopWatchButton";
+import { displayTime } from "./util/displayTime";
+import { LapsList } from "./Components/LapsList";
+// Define the StopWatch component
export default function StopWatch() {
+ // State for managing the stopwatch status
+ const [stopWatchState, setStopWatchState] = useState("begin"); // Can be: begin, running, paused, stopped
+ const [elapsedTime, setElapsedTime] = useState(0); // Will store the elapsed time in seconds
+ const [laps, setLaps] = useState(null); // State to store lap times
+
+ // useEffect hook to handle the stopwatch functionality
+ useEffect(() => {
+ let intervalObj: ReturnType | null;
+ if (stopWatchState == "running") {
+ // Set an interval to increment the elapsed time every second
+ intervalObj = setInterval(() => {
+ setElapsedTime((prevElapsedTime) => prevElapsedTime + 1);
+ }, 1000);
+ }
+ return () => {
+ // Clear the interval when the component unmounts or stopWatchState changes
+ if (intervalObj) {
+ clearInterval(intervalObj);
+ }
+ };
+ }, [stopWatchState, elapsedTime]);
+
+ // Function to handle starting the stopwatch
+ const handleStart = () => {
+ setStopWatchState("running");
+ };
+
+ // Function to handle stopping the stopwatch
+ const handleStop = () => {
+ setStopWatchState("stopped");
+ };
+
+ // Function to handle pausing the stopwatch
+ const handlePause = () => {
+ setStopWatchState("paused");
+ };
+
+ // Function to handle resetting the stopwatch
+ const handleReset = () => {
+ setStopWatchState("begin");
+ setElapsedTime(0);
+ setLaps(null);
+ };
+
+ // Function to handle recording a lap
+ const handleLap = () => {
+ if (stopWatchState == "running") {
+ laps
+ ? setLaps((prevState) => [...prevState, elapsedTime])
+ : setLaps([elapsedTime]);
+ }
+ };
+
+ // Render the stopwatch UI
return (
-
+
+
+ {stopWatchState !== "stopped" && ( // Only display the time if stopwatch is not stopped
+ {displayTime(elapsedTime)}
+ )}
+
+
+ {/* Render buttons based on the current state of the stopwatch */}
+ {(stopWatchState === "begin" || stopWatchState === "stopped") && (
+
+ )}
+ {stopWatchState === "running" && (
+
+ )}
+
+ {stopWatchState === "paused" && (
+
+ )}
+ {stopWatchState === "running" && (
+
+ )}
+
+
+
+
+
+ {/* Render the list of laps */}
+
);
-}
\ No newline at end of file
+}
+
+// StyleSheet for styling the component
+const styles = StyleSheet.create({
+ stopWatchContainer: {
+ flex: 1,
+ paddingBottom: 40,
+ },
+ timeContainer: {
+ flex: 0.6,
+ marginTop: 30,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ timeText: {
+ fontSize: 50,
+ fontWeight: "bold",
+ justifyContent: "center",
+ textAlign: "center",
+ color: "#333",
+ },
+ buttonContainer: {
+ flexDirection: "row",
+ justifyContent: "space-around",
+ marginBottom: 20,
+ },
+});
diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx
index 8768555..b7edf31 100644
--- a/src/StopWatchButton.tsx
+++ b/src/StopWatchButton.tsx
@@ -1,8 +1,40 @@
-import { View } from 'react-native';
+import { View, TouchableOpacity, StyleSheet, Text } from "react-native";
+import { useContext, useState, useEffect } from "react";
-export default function StopWatchButton() {
+interface ButtonProps {
+ handlePress: () => void; // Function to be called when the button is pressed
+ label: string; // Text label for the button
+ backgroundColor: string; // Background color for the button (hex)
+}
+
+// Functional component for a customizable stopwatch button
+export default function StopWatchButton({
+ handlePress,
+ label,
+ backgroundColor,
+}: ButtonProps) {
return (
-
+
+ {/* TouchableOpacity is used to make the View respond to touches */}
+
+ {/* Text of the button */}
+ {label}
+
);
-}
\ No newline at end of file
+}
+
+const styles = StyleSheet.create({
+ btn: {
+ padding: 10,
+ borderRadius: 5,
+ margin: 8,
+ backgroundColor: "#64cc6e", // Default background color, overridden by props if provided
+ },
+ btnLabel: {
+ fontSize: 20,
+ fontWeight: "bold",
+ color: "#fff",
+ textAlign: "center",
+ },
+});
diff --git a/src/util/displayTime.tsx b/src/util/displayTime.tsx
new file mode 100644
index 0000000..627aee7
--- /dev/null
+++ b/src/util/displayTime.tsx
@@ -0,0 +1,12 @@
+export function displayTime(elapsedTime: number): string {
+ // Calculations (elapsedTime is in seconds)
+ // Hours
+ const hours = Math.floor(elapsedTime / 3600);
+ // Minutes
+ const minutes = Math.floor((elapsedTime % 3600) / 60);
+ // Seconds
+ const seconds = Math.floor(elapsedTime % 60);
+ return `${hours.toString().padStart(2, "0")}:${minutes
+ .toString()
+ .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+}
diff --git a/tests/Stopwatch.test.js b/tests/Stopwatch.test.js
index d5e9f1f..39cd570 100644
--- a/tests/Stopwatch.test.js
+++ b/tests/Stopwatch.test.js
@@ -1,55 +1,55 @@
-import React from 'react';
-import { render, fireEvent } from '@testing-library/react-native';
-import Stopwatch from '../src/Stopwatch';
+import React from "react";
+import { render, fireEvent, act } from "@testing-library/react-native";
+import Stopwatch from "../src/Stopwatch";
-describe('Stopwatch', () => {
- test('renders initial state correctly', () => {
+describe("Stopwatch", () => {
+ test("renders initial state correctly", () => {
const { getByText, queryByTestId } = render();
-
- expect(getByText('00:00:00')).toBeTruthy();
- expect(queryByTestId('lap-list')).toBeNull();
+ expect(getByText("00:00:00")).toBeTruthy();
+ expect(queryByTestId("lap-list")).toBeNull();
});
- test('starts and stops the stopwatch', () => {
+ test("starts and stops the stopwatch", () => {
const { getByText, queryByText } = render();
-
- fireEvent.press(getByText('Start'));
+
+ fireEvent.press(getByText("Start"));
expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeTruthy();
- fireEvent.press(getByText('Stop'));
+ fireEvent.press(getByText("Stop"));
expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeNull();
});
- test('pauses and resumes the stopwatch', () => {
+ test("pauses and resumes the stopwatch", async () => {
+ jest.useFakeTimers();
const { getByText } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Pause'));
- const pausedTime = getByText(/(\d{2}:){2}\d{2}/).textContent;
-
- fireEvent.press(getByText('Resume'));
- expect(getByText(/(\d{2}:){2}\d{2}/).textContent).not.toBe(pausedTime);
+ fireEvent.press(getByText("Start"));
+ fireEvent.press(getByText("Pause"));
+ const pausedTime = getByText(/(\d{2}:){2}\d{2}/).props.children;
+ fireEvent.press(getByText("Resume"));
+ await act(async () => jest.advanceTimersByTime(1000));
+ expect(getByText(/(\d{2}:){2}\d{2}/).props.children).not.toBe(pausedTime);
+ jest.clearAllTimers();
});
- test('records and displays lap times', () => {
+ test("records and displays lap times", () => {
const { getByText, getByTestId } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Lap'));
- expect(getByTestId('lap-list')).toContainElement(getByText(/(\d{2}:){2}\d{2}/));
- fireEvent.press(getByText('Lap'));
- expect(getByTestId('lap-list').children.length).toBe(2);
+ fireEvent.press(getByText("Start"));
+ fireEvent.press(getByText("Lap"));
+ expect(getByTestId("lap-list").children.length).toBe(1);
+
+ fireEvent.press(getByText("Lap"));
+ expect(getByTestId("lap-list").children.length).toBe(2);
});
- test('resets the stopwatch', () => {
+ test("resets the stopwatch", () => {
const { getByText, queryByTestId } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Lap'));
- fireEvent.press(getByText('Reset'));
- expect(getByText('00:00:00')).toBeTruthy();
- expect(queryByTestId('lap-list')).toBeNull();
+ fireEvent.press(getByText("Start"));
+ fireEvent.press(getByText("Lap"));
+ fireEvent.press(getByText("Reset"));
+
+ expect(getByText("00:00:00")).toBeTruthy();
+ expect(queryByTestId("lap-list")).toBeNull();
});
});