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
2 changes: 2 additions & 0 deletions dist/app.bundle.js

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions dist/app.bundle.js.LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
1 change: 1 addition & 0 deletions dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="keywords" content="Shopify, StopWatch, JavaScript"/><meta name="author" content="David Cho"/><title>Shopify StopWatch</title><script defer="defer" src="app.bundle.js"></script></head><body><section id="root"></section><script src="app.bundle.js"></script></body></html>
22,256 changes: 16,111 additions & 6,145 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@shopify/polaris": "^12.11.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.21.8",
"@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,8 +37,11 @@
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^24.0.0",
"react-test-renderer": "^18.2.0",
"style-loader": "^3.3.2",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
Expand All @@ -46,6 +54,7 @@
"jest": {
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
},
"testEnvironment": "jsdom"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
21 changes: 13 additions & 8 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Template Site</title>
</head>
<body>
<section id='root'></section>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="keywords" content="Shopify, StopWatch, JavaScript" />
<meta name="author" content="David Cho" />
<title>Shopify StopWatch</title>
</head>
<body>
<section id="root"></section>
<script src="app.bundle.js"></script>
</body>
</html>
16 changes: 11 additions & 5 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 "@shopify/polaris/build/esm/styles.css";
import { AppProvider } from "@shopify/polaris";
import StopWatch from "./StopWatch";
import enTranslations from '@shopify/polaris/locales/en.json';

export default function App() {
return(
<div></div>
)
}
return (
<AppProvider i18n={enTranslations}>
<StopWatch />
</AppProvider>
);
}
117 changes: 112 additions & 5 deletions src/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,114 @@
import React from 'react'
import React, { useState, useEffect } from "react";
import StopWatchButton from "./StopWatchButton";

export function formatTime(time: number): string {
const hours = Math.floor(time / 360000);
const minutes = Math.floor((time % 360000) / 6000);
const seconds = Math.floor((time % 6000) / 100);
const milliseconds = time % 100;

return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}:${milliseconds.toString().padStart(2, "0")}`;
}

export default function StopWatch() {
return(
<div></div>
)
}
// state for time
const [time, setTime] = useState(0);
// state to check if stopwatch is running
const [isRunning, setIsRunning] = useState(false);
// state to check for laps
const [laps, setLaps] = useState([]);

useEffect(() => {
let interval: number;
if (isRunning) {
interval = window.setInterval(() => setTime(time + 1), 10);
}
return () => clearInterval(interval);
}, [isRunning, time]);

// time caclulation for hours, minutes, seconds, and milliseconds
const hours = Math.floor(time / 360000);
const minutes = Math.floor((time % 360000) / 6000);
const seconds = Math.floor((time % 6000) / 100);
const milliseconds = time % 100;

// start and stop time
const startAndStop = () => {
setIsRunning(!isRunning);
};

// reset timer to 0
const reset = () => {
if (isRunning) {
setTime(0);
setIsRunning(!isRunning);
setLaps([]);
} else {
setTime(0);
setIsRunning(isRunning);
setLaps([]);
}
};

// record lap time
const recordLap = () => {
if (isRunning) {
setLaps([...laps, time]);
}
};

return (
<div className="flex flex-col items-center justify-center bg-cover bg-center">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Shopify_Logo.png/640px-Shopify_Logo.png"
style={{
width: "20%",
height: "30%",
padding: "30px",
marginTop: "30px",
}}
alt="Shopify"
/>{" "}
<h1 className="text-center text-3xl font-bold text-gray-800 mt-8">
Shopify Stopwatch
</h1>
<div className="text-4xl font-semibold text-gray-700 mt-8">
{hours.toString().padStart(2, "0")}:
{minutes.toString().padStart(2, "0")}:
{seconds.toString().padStart(2, "0")}:
{milliseconds.toString().padStart(2, "0")}
</div>
{/* pass props to StopWatchButton */}
<div className=" space-x-4 mt-4">
<StopWatchButton
start={"Start"}
stop={"Stop"}
reset={"Reset"}
lap={"Lap"}
isRunning={isRunning}
onStartStop={startAndStop}
onReset={reset}
onLap={recordLap}
/>
</div>
<div>
{/* display laps */}
{laps.length > 0 && (
<div className="mt-8 max-h-64 overflow-auto">
{" "}
<p className="text-xl font-medium">Laps:</p>
<ul className="list-decimal list-inside">
{laps.map((lapTime, index) => (
<li key={index} className="text-gray-600">
Lap {index + 1}: {formatTime(lapTime)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
}
68 changes: 62 additions & 6 deletions src/StopWatchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,63 @@
import React from 'react'
import { ButtonGroup, Button } from "@shopify/polaris";
import React from "react";

export default function StopWatchButton() {
return(
<div></div>
)
}
// prop types for StopWatchButton
type ButtonProps = {
start: string;
stop: string;
reset: string;
lap: string;
isRunning: boolean;
onStartStop: () => void;
onReset: () => void;
onLap: () => void;
};

export default function StopWatchButton({
start,
stop,
reset,
lap,
isRunning,
onStartStop,
onReset,
onLap,
}: ButtonProps) {
return (
// button group for start, stop, reset, and lap buttons
// button group from polaris
<div className="my-4 mx-2">
<ButtonGroup variant="segmented">
{/* start stop button */}
<Button
onClick={onStartStop}
accessibilityLabel={isRunning ? "Stop" : "Start"}
textAlign="center"
size="large"
tone={isRunning ? "critical" : "success"}
>
{isRunning ? stop : start}
</Button>
{/* reset button */}
<Button
onClick={onReset}
accessibilityLabel="Reset"
textAlign="center"
size="large"
>
{reset}
</Button>
{/* lap button */}
<Button
onClick={onLap}
accessibilityLabel="Lap"
disabled={!isRunning}
textAlign="center"
size="large"
>
{lap}
</Button>
</ButtonGroup>
</div>
);
}
76 changes: 76 additions & 0 deletions src/__tests__/components/stopWatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { AppProvider } from "@shopify/polaris";
import enTranslations from "@shopify/polaris/locales/en.json";
import StopWatch from "../../StopWatch";

beforeAll(() => {
window.matchMedia = jest.fn().mockImplementation((query) => {
return {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});
});

describe("StopWatch", () => {
let startStopButton, resetButton, lapButton;

it("renders without crashing", () => {
render(
<AppProvider i18n={enTranslations}>
<StopWatch />
</AppProvider>
);
const elements = screen.getAllByText("Shopify Stopwatch");
expect(elements.length).toBe(2); // assert that there are exactly two elements
});

beforeEach(() => {
render(
<AppProvider i18n={enTranslations}>
<StopWatch />
</AppProvider>
);
startStopButton = screen.getByText(/start/i);
resetButton = screen.getByText(/reset/i);
lapButton = screen.getByText(/lap/i);
});

it("starts the stopwatch", () => {
fireEvent.click(startStopButton);
expect(startStopButton).toHaveTextContent(/stop/i);
});

it("stops the stopwatch", () => {
fireEvent.click(startStopButton);
fireEvent.click(startStopButton);
expect(startStopButton).toHaveTextContent(/start/i);
});

it("resets the stopwatch", () => {
fireEvent.click(startStopButton);
fireEvent.click(resetButton);
expect(screen.getByText(/0{2}\s?:\s?0{2}\s?:\s?0{2}\s?:\s?0{2}/)).toBeInTheDocument();
});

it("records laps", async () => {
fireEvent.click(startStopButton);
fireEvent.click(lapButton);
fireEvent.click(lapButton);

// Wait for the laps to be recorded
const laps = await screen.findAllByText(/lap/i);
expect(laps.length).toBe(1);
});


it("does not record lap when not running", () => {
fireEvent.click(lapButton);
expect(screen.queryByText(/lap 1:/i)).not.toBeInTheDocument();
});
});
Loading