Skip to content
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
Binary file added .DS_Store
Binary file not shown.
2,465 changes: 1,609 additions & 856 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"antd": "^5.13.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand All @@ -20,11 +21,14 @@
"@babel/preset-env": "^7.23.8",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.11",
"@types/node": "^16.18.30",
"@types/react": "^17.0.59",
"@types/react-dom": "^18.2.4",
"@types/react": "^17.0.75",
"@types/react-dom": "^18.2.18",
"@types/testing-library__user-event": "^4.2.0",
"babel-jest": "^29.7.0",
"babel-loader": "^8.3.0",
"css-loader": "^6.7.3",
Expand All @@ -35,12 +39,12 @@
"jest-environment-jsdom": "^29.7.0",
"react-test-renderer": "^18.2.0",
"style-loader": "^3.3.2",
"ts-jest": "^29.1.1",
"ts-jest": "^29.1.2",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"webpack": "^5.82.1",
"webpack": "^5.89.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.15.0",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.8.0",
"webpack-obj-loader": "^1.0.4"
},
Expand Down
70 changes: 70 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// App.test.jsx
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
import '@testing-library/jest-dom/extend-expect';

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

test('renders the stopwatch application', () => {
render(<App />);
expect(screen.getByText('Welcome to my Stopwatch Application')).toBeInTheDocument();
});

test('starts and records laps correctly', () => {
render(<App />);
const startButton = screen.getByText('Start');
userEvent.click(startButton);
act(() => {
jest.advanceTimersByTime(1500);
});

const lapButton = screen.getByText('Lap');
userEvent.click(lapButton);
expect(screen.getByTestId('lap-time-1')).toHaveTextContent('1');

act(() => {
jest.advanceTimersByTime(2000);
});

userEvent.click(lapButton);
expect(screen.getByTestId('lap-time-2')).toHaveTextContent('2');
});

test('stops timer correctly', () => {
render(<App />);
const startButton = screen.getByText('Start');
userEvent.click(startButton);
act(() => {
jest.advanceTimersByTime(3000);
});

const stopButton = screen.getByText('Stop');
userEvent.click(stopButton);

const display = screen.getByTestId('stopwatch-display');
expect(display).toHaveTextContent('00:00:03.000');
});

test('resets timer correctly', () => {
render(<App />);
const startButton = screen.getByText('Start');
userEvent.click(startButton);
act(() => {
jest.advanceTimersByTime(5000);
});

const resetButton = screen.getByText('Reset');
userEvent.click(resetButton);

const display = screen.getByTestId('stopwatch-display');
expect(display).toHaveTextContent('00:00:00.000');
});
125 changes: 119 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,122 @@
import React from 'react'
import './App.css'
import StopWatch from './StopWatch'
import React, { useState, useEffect } from 'react';
import { makeTimeForm, StopWatch } from './StopWatch';
import StopWatchButton from './StopWatchButton';
import { Button, Flex, Table, Typography, Card } from 'antd';
import type { TableProps } from 'antd';

const { Title } = Typography;

interface DataType {
key: number;
lap: number;
timing: string;
}

export default function App() {
return(
<StopWatch />
const [currentTime, setTime] = useState<number>(0);
const [timer, setTimer] = useState<number | undefined>(undefined);
const [laps, setLaps] = useState<number[]>([]);
const [lastLapTime, setLastLapTime] = useState(0);
const [isLapClicked, setIsLapClicked] = useState(false);

const start = () => {
if (timer) clearInterval(timer);
const newTimer = window.setInterval(() => {
setTime((prevTime) => prevTime + 10);
}, 10);
setTimer(newTimer);
};

const stop = () => {
if (timer) clearInterval(timer);
setTimer(undefined);
};

const reset = () => {
setIsLapClicked(false);
setLaps([]);
if (timer) clearInterval(timer);
setTime(0);
setTimer(undefined);
};

const recordLap = () => {
const lapTime = currentTime - lastLapTime;
setLaps([...laps, lapTime]);
setLastLapTime(currentTime);
};

const columns: TableProps<DataType>['columns'] = [
{
title: 'Lap',
dataIndex: 'lap',
key: 'lap',
render: (text, record) => <span data-testid={`lap-time-${record.lap}`}>{text}</span>,
},
{
title: 'Timing',
dataIndex: 'timing',
key: 'timing',
render: (text) => <span>{text}</span>,
},
];

const lapData: DataType[] = laps.map((lapTime, index) => ({
key: index,
lap: index + 1,
timing: makeTimeForm(lapTime),
}));

const DisplayLap: React.FC = () => (
<Table
columns={columns}
dataSource={lapData}
bordered
title={() => 'Stopwatch lap recording'}
/>
);

useEffect(() => {
return () => {
if (timer) clearInterval(timer);
};
}, [timer]);

const handleLapClick = () => {
setIsLapClicked(true);
recordLap();
};

const baseStyle: React.CSSProperties = {
width: "90%",
alignItems: 'center',
margin: "3rem"
};

return (
<>
<Flex gap="middle" vertical style={baseStyle}>
<Flex vertical={true}>
<Title level={2} style={{ marginBottom: '1em' }}>
Welcome to my Stopwatch Application
</Title>

<div>
<Card style={{ width: 550, marginBottom: '2em' }}>
<Title style={{ textAlign: "center" }}>
<StopWatch time={currentTime} />
</Title>
</Card>
<Flex wrap="wrap" justify='space-evenly' align='center' style={{ marginBottom: "2em" }}>
<Button type="primary" size="large"><StopWatchButton title={"Start"} onClick={start} /></Button>
<Button type="primary" size="large"><StopWatchButton title={"Stop"} onClick={stop} /></Button>
<Button danger size="large"><StopWatchButton title={"Reset"} onClick={reset} /></Button>
<Button size="large"><StopWatchButton title="Lap" onClick={handleLapClick} /></Button>
</Flex>
{isLapClicked && <DisplayLap />}
</div>
</Flex>
</Flex>
</>
)
}
}
93 changes: 26 additions & 67 deletions src/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,36 @@
import React, { useState, useEffect, useCallback } from 'react'
import StopWatchButton from './StopWatchButton'
import React, { useState, useEffect } from 'react';

// Function to format the time. This is necessary since both the time and lap times need to be formatted
export function formatTime(time: number): string {
// Format the time in mm:ss:ms. Display hours only if reached
const hours = Math.floor(time / 360000);
const minutes = Math.floor((time % 360000) / 6000);
const seconds = Math.floor((time % 6000) / 100);
const milliseconds = time % 100;
// Format the minutes, seconds, and milliseconds to be two digits
type TimerProps = {
time: number;
};

const makeTimeForm = (milliseconds: number): string => {
const hours = Math.floor(milliseconds / 3600000);
const minutes = Math.floor((milliseconds % 3600000) / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);
const millisecondsPart = milliseconds % 1000;

const formattedHours = hours.toString().padStart(2, '0');
const formattedMinutes = minutes.toString().padStart(2, '0');
const formattedSeconds = seconds.toString().padStart(2, '0');
const formattedMilliseconds = milliseconds.toString().padStart(2, '0');
// If stopwatch reaches at least an hour, display the hours
if (hours > 0) {
const formattedHours = hours.toString().padStart(2, '0');
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`;
}
// Combine the values into a string
const formattedTime = `${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`;
return formattedTime;
}
const formattedMilliseconds = millisecondsPart.toString().padStart(3, '0');

export default function StopWatch() {
// State to track the time, whether the timer is on/off, and the lap times
const [time, setTime] = useState(0);
const [timerOn, setTimerOn] = useState(false);
const [lapTimes, setLapTimes] = useState<number[]>([]);
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
};

// Stops the timer, resets the time, and clears the lap times. useCallback is used to prevent unnecessary re-renders
const handleReset = useCallback(() => {
setTimerOn(false);
setTime(0);
setLapTimes([]);
}, []);
export function StopWatch({ time }: TimerProps) {
const [formattedTime, setFormattedTime] = useState('00:00:00.000');

// Every time timerOn changes, we start or stop the timer
// useEffect is necessary since setInterval changes the state and we don't want to create an infinite loop
useEffect(() => {
let interval: ReturnType<typeof setInterval> | null = null;
setFormattedTime(makeTimeForm(time));
}, [time]);

if (timerOn) {
interval = setInterval(() => setTime(time => time + 1), 10)
}
return (
<div data-testid="stopwatch-display">
{formattedTime}
</div>
);
};

return () => {clearInterval(interval)} // Clears the interval when the component unmounts or timerOn changes
}, [timerOn])
export { makeTimeForm };

return(
<div className='stopwatch'>
<h1 className='stopwatch-title'>StopWatch</h1>
<div className='stopwatch-content'>
<div className='stopwatch-buttons'>
<StopWatchButton type={'start'} onClick={() => setTimerOn(true)}></StopWatchButton>
<StopWatchButton type={'stop'} onClick={() => setTimerOn(false)}></StopWatchButton>
<StopWatchButton type={'lap'} onClick={() => setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes}></StopWatchButton>
<StopWatchButton type={'reset'} onClick={handleReset} time={time}></StopWatchButton>
</div>
<div className='stopwatch-time'>
<p>{formatTime(time)}</p>
{/* Display the numbered lap times */}
{lapTimes.length > 0 && (
<div className='stopwatch-laptimes'>
<p>Lap times</p>
<ul>
{lapTimes.map((lapTime, index) => {
return <li key={index}>{(index + 1)+'.'} {formatTime(lapTime)}</li>
})}
</ul>
</div>
)}
</div>
</div>
</div>
)
}
Loading