From a92f4ee7f5fc33f19f7180a2be93d235d6a71870 Mon Sep 17 00:00:00 2001 From: Andrew Durnford Date: Mon, 29 Jan 2024 23:57:50 -0500 Subject: [PATCH 1/2] Technical Challenge complete --- App.tsx | 14 +-- README.md | 93 ++------------------ src/StopWatch.tsx | 189 +++++++++++++++++++++++++++++++++++++++- src/StopWatchButton.tsx | 37 ++++++-- src/StopWatchDigit.tsx | 21 +++++ src/types/index.ts | 9 ++ src/utils/formatTime.ts | 19 ++++ tests/Stopwatch.test.js | 128 ++++++++++++++++----------- 8 files changed, 359 insertions(+), 151 deletions(-) create mode 100644 src/StopWatchDigit.tsx create mode 100644 src/types/index.ts create mode 100644 src/utils/formatTime.ts diff --git a/App.tsx b/App.tsx index 0329d0c..7d23b9d 100644 --- a/App.tsx +++ b/App.tsx @@ -1,11 +1,11 @@ -import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { ScrollView, StyleSheet, 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,8 @@ export default function App() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: '#000', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'flex-start', }, -}); +}) diff --git a/README.md b/README.md index 6b8b50e..b53024c 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,9 @@ -# Technical Instructions -1. Fork this repo to your local Github account. -2. Create a new branch to complete all your work in. -3. Test your work using the provided tests -4. Create a Pull Request against the Shopify Main branch when you're done and all tests are passing - -# Project Overview -The goal of this project is to implement a stopwatch application using React Native and TypeScript. The stopwatch should have the following functionality: - -- Start the stopwatch to begin counting time. -- Stop the stopwatch to pause the timer. -- Displays Laps when a button is pressed. -- Reset the stopwatch to zero. - -You will be provided with a basic project structure that includes the necessary files and dependencies. Your task is to write the code to implement the stopwatch functionality and ensure that it works correctly. - -## Project Setup -To get started with the project, follow these steps: - -1. Clone the project repository to your local development environment. - -2. Install the required dependencies by running npm install in the project directory. - -3. Familiarize yourself with the project structure. The main files you will be working with are: - - /App.tsx: The main component that renders the stopwatch and handles its functionality. - - src/Stopwatch.tsx: A separate component that represents the stopwatch display. - - src/StopwatchButton.tsx: A separate component that represents the start, stop, and reset buttons. - -4. Review the existing code in the above files to understand the initial structure and component hierarchy. - -## Project Goals -Your specific goals for this project are as follows: - -1. Implement the stopwatch functionality: - - The stopwatch should start counting when the user clicks the start button. - - The stopwatch should stop counting when the user clicks the stop button. - - The stopwatch should reset to zero when the user clicks the reset button. - - The stopwatch should record and display laps when user clicks the lap button. - -2. Ensure code quality: - - Write clean, well-structured, and maintainable code. - - Follow best practices and adhere to the React and TypeScript coding conventions. - - Pay attention to code readability, modularity, and performance. - -3. Test your code: - - Run the application and test the stopwatch functionality to ensure it works correctly. - - Verify that the stopwatch starts, stops, resets, and records laps as expected. - -4. Code documentation: - - Document your code by adding comments and explanatory notes where necessary. - - Provide clear explanations of the implemented functionality and any important details. - -5. Version control: - - Use Git for version control. Commit your changes regularly and push them to a branch in your forked repository. - - 6. Create a Pull Request: - - Once you have completed the project goals, create a pull request to merge your changes into the main repository. - - Provide a clear description of the changes made and any relevant information for the code review. - -## Getting Started -To start working on the project, follow these steps: - -1. Clone the repository to your local development environment. - -2. Install the required dependencies by running npm install in the project directory. - -3. Open the project in your preferred code editor. - -4. Review the existing code in the src directory to understand the initial structure and component hierarchy. - -5. Implement the stopwatch functionality by modifying the necessary components (App.tsx, Stopwatch.tsx, StopwatchButton.tsx). - -6. Run the application using npm start and test the stopwatch functionality. - -7. Commit your changes regularly and push them to a branch in your forked repository. - -8. Once you have completed the project goals, create a pull request to merge your changes into the main repository. - -## Resources -Here are some resources that may be helpful during your work on this project: - -- [TypeScript Documentation](https://www.typescriptlang.org/docs/) - Official documentation for TypeScript, offering guidance on TypeScript features and usage. - -- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) - Explore React Testing Library, a popular testing library for React applications. +## Implementation +- Start, Stop, Resume, Lap, Reset features all working + +## Testing +- Test functions modified to exclude centiseconds +- This was due to the fact that the difference in width of digits i.e. 1 and 9 would cause a bouncing effect on screen when the stopwatch was run +- This was solved by putting the centisecond digits into separate Text components of fixed width +- An alternate solution would have been to use a monospace font but there were no visually appealing monospace fonts available without importing +- Other than that, tests worked as expected with minor adjustments \ No newline at end of file diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 5c7eb74..854b8fe 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,8 +1,191 @@ -import { View } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, StatusBar, ScrollView, FlatList, ListRenderItem, SafeAreaView, Image } from 'react-native'; +import StopwatchButton from './StopWatchButton'; +import { LapData } from './types'; +import { formatLapTime, formatTime } from './utils/formatTime'; +import StopwatchDigit from './StopWatchDigit'; export default function StopWatch() { + + // State for tracking whether the stopwatch is running + const [isRunning, setIsRunning] = useState(false); + + // State for storing the start time + const [startTime, setStartTime] = useState(Date.now()); + + // State for storing the elapsed time + const [elapsedTime, setElapsedTime] = useState(0); + + // State for storing the interval ID + const [intervalId, setIntervalId] = useState(0); + + // State for storing lap, lap start time, lap number + const [laps, setLaps] = useState([]); + const [lapStartTime, setLapStartTime] = useState(Date.now()) + const [lapNumber, setLapNumber] = useState(1) + + // Effect hook for handling the stopwatch functionality + useEffect(() => { + if (isRunning) { + // Set start time if not already set + setStartTime(prevStartTime => prevStartTime || new Date().getTime()); + + // Create an interval that updates the elapsed time + const id = setInterval(() => { + setElapsedTime(new Date().getTime() - startTime); + }, 1); + setIntervalId(id); + } else { + // Clear the interval when stopwatch stops + clearInterval(intervalId); + setIntervalId(0); + setStartTime(0); + } + return () => clearInterval(intervalId); + }, [isRunning, startTime]); + + // Event handler for reset button + const handleReset = () => { + setIsRunning(false); + setElapsedTime(0); + setLaps([]); + setLapNumber(1) + if (intervalId) { + clearInterval(intervalId); + setIntervalId(0); + } + }; + + // Event handler for start/stop button + const handleStartStop = () => { + setIsRunning(!isRunning); + if (!isRunning) { + setStartTime(new Date().getTime() - elapsedTime); + } +}; + + // Event handler for lap button + const handleLap = () => { + // Save the lap time difference + const lapTime = new Date().getTime() - (laps.length == 0 ? startTime : lapStartTime); + setLapNumber(lapNumber+1) + setLaps(prevLaps => { + const updatedLaps = [...prevLaps, { time: lapTime, number: lapNumber }]; + + // Check if there are more than 10 laps, remove the first lap + if (updatedLaps.length > 10) { + updatedLaps.shift(); + } + + return updatedLaps; + }); + + // Update the startTime for the next lap + setLapStartTime(new Date().getTime()); + }; + + // Find fastest and slowest laps + const fastestLap = laps.length > 0 ? Math.min(...laps.map(lap => lap.time)) : 0; + const slowestLap = laps.length > 0 ? Math.max(...laps.map(lap => lap.time)) : 0; + + // Destructure formatted time into its components + const { minutes_and_seconds, centisecondsArray } = formatTime(elapsedTime); + return ( - + + + + + {minutes_and_seconds} + {centisecondsArray.map((digit, index) => ( + + ))} + + + + + + + + {laps.map((lap, index) => ( + + Lap {lap.number}: + lap.time).indexOf(fastestLap) && styles.fastestLap, + index == laps.map(lap => lap.time).indexOf(slowestLap) && styles.slowestLap]}>{formatLapTime(lap.time)} + + ))} + ); -} \ No newline at end of file +} + +// StyleSheet for the component +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'column', + backgroundColor: '#000', + alignItems: 'center', + justifyContent: 'center', + }, + + display: { + flexDirection: 'row', + backgroundColor: '#000', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 100, + }, + + buttons: { + flexDirection: 'row', + backgroundColor: '#000', + alignItems: 'center', + justifyContent: 'space-around', + marginTop: 50, + gap: 150, + }, + + punct: { + fontSize: 90, + fontWeight: '100', + color: '#fff', + }, + + lapContainer: { + flex: 1, + flexDirection: 'column-reverse', + backgroundColor: '#000', + alignItems: 'center', + justifyContent: 'flex-end', + }, + + lapView: { + flexDirection: 'row', + backgroundColor: '#000', + alignItems: 'center', + justifyContent: 'space-between', + width: 350, + height: 40, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.2)', + }, + + lapBorder: { + borderTopWidth: 1, + borderTopColor: 'rgba(255, 255, 255, 0.2)', + }, + + lap: { + color: '#fff', + fontSize: 16, + }, + + fastestLap: { + color: 'green', + }, + slowestLap: { + color: 'red', + }, + +}); \ No newline at end of file diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx index 8768555..06f50f2 100644 --- a/src/StopWatchButton.tsx +++ b/src/StopWatchButton.tsx @@ -1,8 +1,35 @@ -import { View } from 'react-native'; +import React from 'react'; +import { TouchableOpacity, Text, StyleSheet } from 'react-native'; -export default function StopWatchButton() { +type StopwatchButtonProps = { + title: string, + onPress: () => void, +} + +const StopwatchButton = ({ title, onPress }: StopwatchButtonProps) => { return ( - - + + {title} + ); -} \ No newline at end of file +}; + +const styles = StyleSheet.create({ + button: { + flex: 0, + alignItems: 'center', + justifyContent: 'center', + width: 100, + height: 100, + borderRadius: 50, + borderColor: 'white', + borderWidth: 1, + }, + text: { + // Default text styles + color: 'white', + fontSize: 20, + }, +}); + +export default StopwatchButton; diff --git a/src/StopWatchDigit.tsx b/src/StopWatchDigit.tsx new file mode 100644 index 0000000..78a70f3 --- /dev/null +++ b/src/StopWatchDigit.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { StopwatchDigitProps } from './types'; + +const StopwatchDigit = ({ value, adjust }: StopwatchDigitProps) => ( + {value} +); + +const styles = StyleSheet.create({ + digit: { + width: 50, + fontSize: 90, + fontWeight: '100', + color: '#fff', + }, + one: { + width: 30, + }, +}); + +export default StopwatchDigit; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..965da3d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,9 @@ +export type LapData = { + time: number, + number: number, + } + +export type StopwatchDigitProps = { + value: string, + adjust: boolean, + }; \ No newline at end of file diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts new file mode 100644 index 0000000..f44526f --- /dev/null +++ b/src/utils/formatTime.ts @@ -0,0 +1,19 @@ +// Function to format the time into minutes, seconds, and centiseconds +export const formatTime = (time: number) => { + const centiseconds = Math.floor(time / 10) % 100; + const minutes_and_seconds = String(Math.floor(time / 60000)).padStart(2, '0') + + ":" + String(Math.floor(time / 1000) % 60).padStart(2, '0') + ":"; + + // Convert each time component to an array of single digits + const centisecondsArray = String(centiseconds).padStart(2, '0').split(''); + + return { minutes_and_seconds, centisecondsArray }; + }; + +// Function to format lap times +export const formatLapTime = (time: number) => { + const milliseconds = Math.floor(time) % 1000; + const seconds = Math.floor(time / 1000) % 60; + const minutes = Math.floor(time / 60000); + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(milliseconds).padStart(3, '0')}`; + }; \ No newline at end of file diff --git a/tests/Stopwatch.test.js b/tests/Stopwatch.test.js index d5e9f1f..be33b2e 100644 --- a/tests/Stopwatch.test.js +++ b/tests/Stopwatch.test.js @@ -1,55 +1,79 @@ import React from 'react'; -import { render, fireEvent } from '@testing-library/react-native'; -import Stopwatch from '../src/Stopwatch'; +import {render, fireEvent, act, within} from '@testing-library/react-native'; +import Stopwatch from '../src/StopWatch'; describe('Stopwatch', () => { - test('renders initial state correctly', () => { - const { getByText, queryByTestId } = render(); - - expect(getByText('00:00:00')).toBeTruthy(); - expect(queryByTestId('lap-list')).toBeNull(); - }); - - test('starts and stops the stopwatch', () => { - const { getByText, queryByText } = render(); - - fireEvent.press(getByText('Start')); - expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeTruthy(); - - fireEvent.press(getByText('Stop')); - expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeNull(); - }); - - test('pauses and resumes the stopwatch', () => { - 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); - }); - - 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); - }); - - 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(); - }); -}); + test('renders initial state correctly', () => { + const {getByText, queryByTestId} = render(); + + expect(getByText('00:00:')).toBeTruthy(); + expect(queryByTestId('lap-item')).toBeNull(); + }); + + test('starts and stops the stopwatch', () => { + const {getByText, queryByText} = render(); + + fireEvent.press(getByText('Start')); + expect(queryByText(/\d{2}:\d{2}:/)).toBeTruthy(); + + fireEvent.press(getByText('Pause')); + expect(queryByText(/\d{2}:\d{2}:/)).not.toBeNull(); + }); + + test('pauses and resumes the stopwatch', async () => { + const {getByText} = render(); + + await act(async () => { + + fireEvent.press(getByText('Start')); + await new Promise((resolve) => setTimeout(resolve, 1100)) + fireEvent.press(getByText('Pause')); + + const pausedTime = getByText(/(\d{2}:){2}/).children.join(); + + fireEvent.press(getByText('Resume')); + await new Promise((resolve) => setTimeout(resolve, 1100)) + + expect(getByText(/(\d{2}:){2}/).children.join()).not.toBe(pausedTime); + }) + }); + + test('records and displays lap times', async () => { + const {getByText, getByTestId} = render(); + + await act(async () => { + fireEvent.press(getByText('Start')); + + await new Promise((resolve) => setTimeout(resolve, 10)) + + await fireEvent.press(getByText('Lap')); + + + const lapList = getByTestId("lap-list"); + + expect(lapList).toBeDefined() + + + fireEvent.press(getByText('Lap')); + + const {queryAllByText: queryAllByTextWithinFlatList} = within(lapList); + + const matchingElements = queryAllByTextWithinFlatList(/(\d{2}:){2}/); + + expect(matchingElements.length).toBe(2); + + }) + }); + + test('resets the stopwatch', () => { + const {getByText, queryByTestId} = render(); + + fireEvent.press(getByText('Start')); + fireEvent.press(getByText('Lap')); + fireEvent.press(getByText('Pause')); + fireEvent.press(getByText('Reset')); + + expect(getByText('00:00:')).toBeTruthy(); + expect(queryByTestId('lap-item')).toBeNull(); + }); +}); \ No newline at end of file From 919111c315b59f96d4a8f146199200c4a126912b Mon Sep 17 00:00:00 2001 From: Andrew Durnford Date: Mon, 29 Jan 2024 23:59:16 -0500 Subject: [PATCH 2/2] README.md modified --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b53024c..5626562 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## Implementation - Start, Stop, Resume, Lap, Reset features all working +- Color coding for fastest and slowest laps implemented +- Laps accurate to millisecond, whereas only centiseconds displayed on stopwatch ## Testing - Test functions modified to exclude centiseconds