diff --git a/package.json b/package.json index cf0d970a..feb710c4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@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", "@types/jest": "^29.5.11", "@types/node": "^16.18.30", @@ -32,6 +33,7 @@ "html-loader": "^4.2.0", "html-webpack-plugin": "^5.5.1", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "react-test-renderer": "^18.2.0", "style-loader": "^3.3.2", "ts-jest": "^29.1.1", @@ -46,6 +48,7 @@ "jest": { "transform": { "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" - } + }, + "testEnvironment": "jsdom" } } diff --git a/public/index.html b/public/index.html index 5953d96e..f90999df 100755 --- a/public/index.html +++ b/public/index.html @@ -1,9 +1,18 @@ - - - Template Site - - -
- + + + + + + StopWatch App + + + + + + +
+ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 95351af9..58a8fb3a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,13 @@ -import React from 'react' +import React from 'react'; +import Stopwatch from './StopWatch'; +import './assets/App.css'; -export default function App() { - return( -
- ) -} \ No newline at end of file +const App: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index d58edc78..4c2024ff 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,7 +1,82 @@ -import React from 'react' +import React, { useState, useEffect } from 'react'; +import './assets/StopWatch.css'; +import StopwatchTimer from './StopWatchTimer'; +import { formatTime } from './utils'; +import StopwatchButton from './StopWatchButton'; +import StopwatchLaps from './StopWatchLaps'; -export default function StopWatch() { - return( -
- ) -} \ No newline at end of file +interface StopwatchProps { } + +const Stopwatch: React.FC = () => { + const [time, setTime] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const [laps, setLaps] = useState([]); + + useEffect(() => { + let intervalId: NodeJS.Timeout; + + if (isRunning) { + intervalId = setInterval(() => { + setTime((prevTime) => prevTime + 10); + }, 10); + } + + return () => clearInterval(intervalId); + }, [isRunning]); + + const startStopwatch = () => { + setIsRunning(true); + }; + + const stopStopwatch = () => { + setIsRunning(false); + }; + + const resetStopwatch = () => { + setTime(0); + setIsRunning(false); + setLaps([]); + }; + + const recordLap = () => { + setLaps((prevLaps) => [...prevLaps, time]); + }; + + return ( +
+ +
+ {isRunning ? ( + <> + + + + ) : ( + <> + + + + )} +
+ +
+ ); +}; + +export default Stopwatch; diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx index dbd7c174..78ef0484 100644 --- a/src/StopWatchButton.tsx +++ b/src/StopWatchButton.tsx @@ -1,7 +1,17 @@ -import React from 'react' +import React from 'react'; -export default function StopWatchButton() { - return( -
- ) -} \ No newline at end of file +interface StopwatchButtonProps { + label: string; + className: string; + onClick: () => void; +} + +const StopwatchButton: React.FC = (props: StopwatchButtonProps) => { + return ( + + ); +}; + +export default StopwatchButton; \ No newline at end of file diff --git a/src/StopWatchLaps.tsx b/src/StopWatchLaps.tsx new file mode 100644 index 00000000..3fed181d --- /dev/null +++ b/src/StopWatchLaps.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { formatTime } from './utils'; + +interface StopwatchLapsProps { + laps: number[]; +} + +const StopwatchLaps: React.FC = (props: StopwatchLapsProps) => { + return ( +
+ {props.laps.map((lapTime, index) => ( +
{`Lap ${index + 1} - ${formatTime(lapTime)}`}
+ ))} +
+ ); +}; + +export default StopwatchLaps; \ No newline at end of file diff --git a/src/StopWatchTimer.tsx b/src/StopWatchTimer.tsx new file mode 100644 index 00000000..01f28c4f --- /dev/null +++ b/src/StopWatchTimer.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { formatTime } from './utils'; + +interface StopwatchTimerProps { + time: number; + } + +const StopwatchTimer: React.FC = (props) => { + + return ( +
{formatTime(props.time)}
+ ); +}; + +export default StopwatchTimer; \ No newline at end of file diff --git a/src/assets/App.css b/src/assets/App.css new file mode 100644 index 00000000..eb4f6ab8 --- /dev/null +++ b/src/assets/App.css @@ -0,0 +1,31 @@ +@import 'common.css'; + +body { + background-color: #eeddd3; + margin: 0; + padding: 0; + font-family: 'Helvetica Neue', sans-serif; + color: #dcdcdc; +} + +.app-container { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + background-color: #eeddd3; + padding: 20px; + box-sizing: border-box; +} + +/* Responsive Layout */ +@media screen and (min-width: 768px) { + .app-container { + max-width: 800px; + margin: 0 auto; + } +} + +.stopwatch-container { + text-align: center; +} \ No newline at end of file diff --git a/src/assets/StopWatch.css b/src/assets/StopWatch.css new file mode 100644 index 00000000..47d267d2 --- /dev/null +++ b/src/assets/StopWatch.css @@ -0,0 +1,13 @@ +/* Stopwatch.css */ + +@import 'common.css'; + +.stopwatch-container { + max-width: 400px; + margin: 0 auto; + text-align: center; + background-color: #fff1e6; + padding: 30px; + border-radius: 15px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/src/assets/common.css b/src/assets/common.css new file mode 100644 index 00000000..a0ca2abd --- /dev/null +++ b/src/assets/common.css @@ -0,0 +1,71 @@ +.timer { + font-size: 3em; + margin: 20px 0; + color: #3b3b3b; +} + +.button-container { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + margin-bottom: 20px; + position: relative; + /* Position relative to make it a reference point for pseudo-element */ +} + +/* Button Styles */ +.start-button, +.stop-button, +.reset-button, +.lap-button { + width: 100px; + height: 100px; + padding: 15px; + font-size: 1.2em; + cursor: pointer; + border: none; + border-radius: 50%; + transition: background-color 0.5s ease; + margin: 40px; +} + +.button-container::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 1px; + /* Adjust the height for the thickness of the line */ + background-color: rgba(59, 59, 59, 0.3); +} + +.start-button { + background-color: #6A9C89; + color: white; +} + +.stop-button { + background-color: #9D2503; + color: white; +} + +.reset-button { + background-color: #CD5C08; + color: white; +} + +.lap-button { + background-color: #3b3b3b; + color: white; +} + +.lap-list { + text-align: center; + margin-top: 20px; +} + +.lap-item { + margin: 20px; + color: #808080; +} \ No newline at end of file diff --git a/src/tests/StopwatchButton.test.tsx b/src/tests/StopwatchButton.test.tsx new file mode 100644 index 00000000..411f0863 --- /dev/null +++ b/src/tests/StopwatchButton.test.tsx @@ -0,0 +1,41 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import StopwatchButton from '../StopWatchButton'; + +describe('StopwatchButton Component', () => { + const mockOnClick = jest.fn(); + + const renderComponent = (label: string, className: string) => { + render( + + ); + }; + + test('renders button with provided label and className', () => { + const label = 'Start'; + const className = 'start-button'; + + renderComponent(label, className); + + const button = screen.getByText(label); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass(className); + }); + + test('calls onClick handler when the button is clicked', () => { + const label = 'Start'; + const className = 'start-button'; + + renderComponent(label, className); + + const button = screen.getByText(label); + fireEvent.click(button); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..78956825 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,13 @@ +export const formatTime = (milliseconds: number): string => { + const hours = Math.floor(milliseconds / 3600000); + const minutes = Math.floor((milliseconds % 3600000) / 60000); + const seconds = Math.floor((milliseconds % 60000) / 1000); + const remainingMilliseconds = Math.floor((milliseconds % 1000) / 10); + + return `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}:${padZero(remainingMilliseconds)}`; +}; + +export const padZero = (num: number, length: number = 2): string => { + const str = num.toString(); + return str.length >= length ? str : '0'.repeat(length - str.length) + str; +};