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(); }); });