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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -46,6 +48,7 @@
"jest": {
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
},
"testEnvironment": "jsdom"
}
}
23 changes: 16 additions & 7 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Template Site</title>
</head>
<body>
<section id='root'></section>
</body>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>StopWatch App</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet">
</head>

<body>
<div id="root"></div>
</body>

</html>
18 changes: 12 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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(
<div></div>
)
}
const App: React.FC = () => {
return (
<div className="app-container">
<Stopwatch />
</div>
);
};

export default App;
87 changes: 81 additions & 6 deletions src/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -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(
<div></div>
)
}
interface StopwatchProps { }

const Stopwatch: React.FC<StopwatchProps> = () => {
const [time, setTime] = useState<number>(0);
const [isRunning, setIsRunning] = useState<boolean>(false);
const [laps, setLaps] = useState<number[]>([]);

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 (
<div className="stopwatch-container">
<StopwatchTimer time={time} />
<div className="button-container">
{isRunning ? (
<>
<StopwatchButton
label={"Stop"}
className={"stop-button"}
onClick={stopStopwatch}
/>
<StopwatchButton
label={"Lap"}
className={"lap-button"}
onClick={recordLap}
/>
</>
) : (
<>
<StopwatchButton
label={"Start"}
className={"start-button"}
onClick={startStopwatch}
/>
<StopwatchButton
label={"Reset"}
className={"reset-button"}
onClick={resetStopwatch}
/>
</>
)}
</div>
<StopwatchLaps laps={laps} />
</div>
);
};

export default Stopwatch;
22 changes: 16 additions & 6 deletions src/StopWatchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import React from 'react'
import React from 'react';

export default function StopWatchButton() {
return(
<div></div>
)
}
interface StopwatchButtonProps {
label: string;
className: string;
onClick: () => void;
}

const StopwatchButton: React.FC<StopwatchButtonProps> = (props: StopwatchButtonProps) => {
return (
<button className={props.className} onClick={props.onClick}>
{props.label}
</button>
);
};

export default StopwatchButton;
18 changes: 18 additions & 0 deletions src/StopWatchLaps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { formatTime } from './utils';

interface StopwatchLapsProps {
laps: number[];
}

const StopwatchLaps: React.FC<StopwatchLapsProps> = (props: StopwatchLapsProps) => {
return (
<div className="lap-list">
{props.laps.map((lapTime, index) => (
<div key={index} className="lap-item">{`Lap ${index + 1} - ${formatTime(lapTime)}`}</div>
))}
</div>
);
};

export default StopwatchLaps;
15 changes: 15 additions & 0 deletions src/StopWatchTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { formatTime } from './utils';

interface StopwatchTimerProps {
time: number;
}

const StopwatchTimer: React.FC<StopwatchTimerProps> = (props) => {

return (
<div className="timer">{formatTime(props.time)}</div>
);
};

export default StopwatchTimer;
31 changes: 31 additions & 0 deletions src/assets/App.css
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions src/assets/StopWatch.css
Original file line number Diff line number Diff line change
@@ -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);
}
71 changes: 71 additions & 0 deletions src/assets/common.css
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions src/tests/StopwatchButton.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<StopwatchButton
label={label}
className={className}
onClick={mockOnClick}
/>
);
};

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);
});
});
Loading