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;
+};