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..5626562 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,11 @@
-# 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
+- 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
+- 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