Skip to content

David Yang Shopify Front-End Challenge #366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
85 changes: 27 additions & 58 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,71 +1,40 @@
html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons {
height: -webkit-fill-available;
}

body {
background-color: #f1f1f1;
margin: 0px;
}

.stopwatch-title {
background-color: #303030;
margin: 0px;
color: white;
padding-left: 16px;
padding: 10px 0px 10px 16px;
}

.stopwatch-content {
display: flex;
margin: 0;
background-color: lightgreen;
}

.stopwatch-buttons {
.stopwatch-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #ebebeb;
padding: 16px 12px;
width: 200px;
}

.stopwatch-buttons button:focus {
outline: none;
border: 2px solid #000000;
justify-content: center;
align-items: center;
}

.stopwatch-buttons button {
margin: 7px 0px;
background-color: #fafafa;
border: 0px solid #fafafa;
text-align: left;
border-radius: 0.5rem;
padding: 7px 0px 7px 15px;
box-shadow: 0.5px 0.5px gray;
}

.stopwatch-time {
margin-left: auto;
margin-right: auto;
margin-top: 20px;
padding: 50px;
background-color: #ffffff;
height: fit-content;
border-radius: 0.75rem;
width: 50%;
text-align: -webkit-center;
box-shadow: 0.5px 0.5px gray;
.timer-display {
display: flex;
color: #ccc;
}

.stopwatch-time p {
font-size: xxx-large;
.timer-display p {
margin: 0;
font-size: 4rem;
letter-spacing: 2px;
}

.stopwatch-laptimes ul {
list-style: none;
padding: 0px;
.timer-display span {
font-size: 3rem;
position: relative;
top: 8px;
letter-spacing: 2px;
}

.stopwatch-laptimes li {
padding: 10px 0px;
border-bottom: 1px solid #ebebeb;
font-size: x-large;
}
.stopwatch-controls-container button {
margin: 15px;
border: none;
background-color: #e5ad3d;
padding: 3px 12px;
font-size: 1.3rem;
font-weight: 400;
cursor: pointer;
}
108 changes: 102 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,105 @@
import React from 'react'
import './App.css'
import StopWatch from './StopWatch'
import React, { useEffect, useState } from 'react';
import StopWatch from './StopWatch';
import './css/App.css';
import StopWatchButton from './StopWatchButton';

export default function App() {
return(
<StopWatch />
// State variables for managing stopwatch functionality
const [isRunning, setIsRunning] = useState<boolean>(false);
const [reset, setReset] = useState<boolean>(false);
const [time, setTime] = useState<number>(0);
const [timeDisplay, setTimeDisplay] = useState<Array<number | string>>([]);
const [laps, setLaps] = useState<Array<number>>([]);

// Converts time in seconds to a displayable format (HH:MM:SS)
const convertTimeToDisplay = (time: number): (number | string)[] => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time - hours * 3600) / 60);
const seconds = time - minutes * 60 - hours * 3600;
return [
hours < 10 ? `0${hours}` : hours,
minutes < 10 ? `0${minutes}` : minutes,
seconds < 10 ? `0${seconds}` : seconds
];
}

// Handles the start button click, sets isRunning to true and initializes laps if the timer has not started
const handleOnStart = () => {
setIsRunning(true);
if (time === 0) {
initializeLaps();
}
}

// Handles the stop button click, sets isRunning to false to pause the timer
const handleOnStop = () => {
setIsRunning(false);
}

// Handles the reset button click, sets reset to true triggering a reset in useEffect
const handleReset = () => {
setReset(true);
}

// Initializes laps with time set to 0
const initializeLaps = () => {
setLaps([0]);
}

// Handles the lap button click, adds the current time to the laps array
const handleLap = () => {
if (time !== 0) {
setLaps((prevLaps) => [...prevLaps, time]);
}
}

// useEffect to manage timer logic and updates
useEffect(() => {
let intervalId: NodeJS.Timeout;

// Increment time every 1000ms if isRunning is true
if (isRunning) {
intervalId = setInterval(() => setTime((prevTime) => prevTime + 1), 1000);
} else {
clearInterval(intervalId); // Stop the timer if isRunning is false
}

// Reset the timer, laps, and display when reset is true
if (reset) {
setIsRunning(false);
setTime(0);
setReset(false);
setLaps([]);
}

// Update time display
setTimeDisplay(convertTimeToDisplay(time));

// Cleanup: clear the interval to avoid memory leaks
return () => {
clearInterval(intervalId);
};
}, [reset, isRunning, time]);

// App display with Stopwatch, buttons, and lap list
return (
<div className='stopwatch-container'>
<StopWatch timeDisplay={timeDisplay} />
<div className='stopwatch-button-container'>
<StopWatchButton type={'Start'} onClick={handleOnStart} />
<StopWatchButton type={'Stop'} onClick={handleOnStop} />
<StopWatchButton type={'Reset'} onClick={handleReset} />
<StopWatchButton type={'Lap'} onClick={handleLap} />
</div>
<div>
<ul className='lap-list'>
{laps.map((lap, index, array) => (
<li key={index} style={{ listStyle: 'None', color: 'whitesmoke' }}>
{index > 0 ? `Lap ${index}: ${lap - array[index - 1]} seconds` : ''}
</li>
))}
</ul>
</div>
</div>
)
}
}
86 changes: 12 additions & 74 deletions src/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react'
import StopWatchButton from './StopWatchButton'

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

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[]>([]);

// 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([]);
}, []);

// 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;

if (timerOn) {
interval = setInterval(() => setTime(time => time + 1), 10)
}

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

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>
import React from 'react';
import './css/App.css';
import { IStopWatchProps } from './type/InterfaceSWT';

export default function StopWatch(props: IStopWatchProps) {
return (
<div className='timer-display'>
<p id="hour">{props.timeDisplay[0]}</p>
<span>:</span>
<p id="minute">{props.timeDisplay[1]}</p>
<span>:</span>
<p id="second">{props.timeDisplay[2]}</p>
</div>
)
}
63 changes: 13 additions & 50 deletions src/StopWatchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,17 @@
import React from 'react'
import React from 'react';
import './css/App.css';
import { IStopWatchProps } from './type/InterfaceSWT';

// Maximum number of laps that can be recorded
const maxLaps = 25;
export default function StopWatch(props: IStopWatchProps) {

// Define the props for the StopWatchButton component
type StopWatchButtonProps = {
type: 'start' | 'stop' | 'lap' | 'reset';
onClick?: () => void;
timerOn?: boolean;
time?: number;
lapTimes?: number[];
};

export default function StopWatchButton({ type, onClick, timerOn, time, lapTimes }: StopWatchButtonProps) {
// Determine the button text based on the type and add corresponding tabIndex
let buttonText, tabIndex;
switch(type) {
case 'start':
buttonText = 'Start';
tabIndex = 1;
break;
case 'stop':
buttonText = 'Stop';
tabIndex = 2;
break;
case 'lap':
buttonText = 'Record Lap';
tabIndex = 3;
break;
case 'reset':
buttonText = 'Reset';
tabIndex = 4;
break;
default:
buttonText = '';
tabIndex = 0;
}
// Determine whether the reset or lap buttons should be disabled
const isLapDisabled = !timerOn || (lapTimes && lapTimes.length === 25);
const isResetDisabled = time === 0;
return(
<button
onClick={onClick}
aria-label={type}
tabIndex={tabIndex}
// Disable the lap button when the timer is stopped or when the max number of lap times is reached. Disable reset button when the timer is already reset
disabled={(type === 'lap' && isLapDisabled) || (type === 'reset' && isResetDisabled)}
>
{/* Display the button text, otherwise display the max laps reached message when max number is reached */}
{lapTimes && lapTimes.length === maxLaps && timerOn && type === 'lap' ? "Maximum laps reached" : buttonText}
</button>
// Stopwatch Display with hours, minutes, seconds
return (
<div className='timer-display'>
<p id="hour">{props.timeDisplay[0]}</p>
<span>:</span>
<p id="minute">{props.timeDisplay[1]}</p>
<span>:</span>
<p id="second">{props.timeDisplay[2]}</p>
</div>
)
}
4 changes: 4 additions & 0 deletions src/type/InterfaceSWBP.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IStopWatchButtonProps {
type: string,
onClick: (e:any) => void,
}
3 changes: 3 additions & 0 deletions src/type/InterfaceSWT.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IStopWatchProps {
timeDisplay: (string | number)[]
}