From 102d9b0a287b2da3650b9ae4328d75851b90b6c0 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 02:44:12 -0500 Subject: [PATCH 1/8] Update page title --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 5953d96e..0395893b 100755 --- a/public/index.html +++ b/public/index.html @@ -1,7 +1,7 @@ - Template Site + Wilson Chu's Frontend Challenge Submission 2024
From 24afc5a4c64965a473c935ed77bce673c58840b2 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 03:46:31 -0500 Subject: [PATCH 2/8] Change page title and background color to Shopify style header --- src/App.css | 3 ++- src/StopWatch.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/App.css b/src/App.css index e5deef95..0a43cf2c 100644 --- a/src/App.css +++ b/src/App.css @@ -8,11 +8,12 @@ body { } .stopwatch-title { - background-color: #303030; + background-color: #96bf48; /* Shopify green */ margin: 0px; color: white; padding-left: 16px; padding: 10px 0px 10px 16px; + text-align: center; } .stopwatch-content { diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 2e06473c..5f805650 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -49,7 +49,7 @@ export default function StopWatch() { return(
-

StopWatch

+

StopWatchify

setTimerOn(true)}> From 6f9476ae0b607de1d14b7f5b5709310bc2bfd840 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 05:20:46 -0500 Subject: [PATCH 3/8] Change title to stopwatchify and convert px to em for responsiveness --- src/App.css | 39 ++++++++++++++++++++++----------------- src/StopWatch.tsx | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/App.css b/src/App.css index 0a43cf2c..78b6f58e 100644 --- a/src/App.css +++ b/src/App.css @@ -1,19 +1,24 @@ +:root { + --shopify-green: #96bf48; + --shopify-dark-green: #769241; +} + html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons { height: -webkit-fill-available; } body { background-color: #f1f1f1; - margin: 0px; + margin: 0; } .stopwatch-title { - background-color: #96bf48; /* Shopify green */ - margin: 0px; + background-color: var(--shopify-green); + margin: 0; color: white; - padding-left: 16px; - padding: 10px 0px 10px 16px; + padding: 1em 0 1em 0; text-align: center; + font-style: italic; } .stopwatch-content { @@ -24,31 +29,31 @@ body { display: flex; flex-direction: column; background-color: #ebebeb; - padding: 16px 12px; - width: 200px; + padding: 1em 0.75em; + width: 12.5em; } .stopwatch-buttons button:focus { outline: none; - border: 2px solid #000000; + border: 1px solid black; } .stopwatch-buttons button { - margin: 7px 0px; + margin: 0.75em 0; background-color: #fafafa; - border: 0px solid #fafafa; + border: 0 solid #fafafa; text-align: left; border-radius: 0.5rem; - padding: 7px 0px 7px 15px; - box-shadow: 0.5px 0.5px gray; + padding: 0.75em 0 0.75em 1em; + box-shadow: 1px 1px gray; } .stopwatch-time { margin-left: auto; margin-right: auto; - margin-top: 20px; - padding: 50px; - background-color: #ffffff; + margin-top: 1.25em; + padding: 3.25em; + background-color: white; height: fit-content; border-radius: 0.75rem; width: 50%; @@ -62,11 +67,11 @@ body { .stopwatch-laptimes ul { list-style: none; - padding: 0px; + padding: 0; } .stopwatch-laptimes li { - padding: 10px 0px; + padding: 0.75em 0; border-bottom: 1px solid #ebebeb; font-size: x-large; } \ No newline at end of file diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 5f805650..b0f2599f 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -49,7 +49,7 @@ export default function StopWatch() { return(
-

StopWatchify

+

stopwatchify

setTimerOn(true)}> From 2eccfbf68917dd247824bfe0f88a3024595efe80 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 22:55:35 -0500 Subject: [PATCH 4/8] Change css values to universal % and rem --- src/App.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.css b/src/App.css index 78b6f58e..71cc3c2f 100644 --- a/src/App.css +++ b/src/App.css @@ -4,7 +4,7 @@ } html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons { - height: -webkit-fill-available; + height: 100%; } body { @@ -57,12 +57,12 @@ body { height: fit-content; border-radius: 0.75rem; width: 50%; - text-align: -webkit-center; + text-align: center; box-shadow: 0.5px 0.5px gray; } .stopwatch-time p { - font-size: xxx-large; + font-size: 4rem; } .stopwatch-laptimes ul { From ef9772b10cd4810c85884b3cccfd0757b8db6a16 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 23:25:16 -0500 Subject: [PATCH 5/8] Add hover effect over clickable buttons --- src/App.css | 4 ++ src/StopWatch.tsx | 8 ++-- src/StopWatchButton.tsx | 94 +++++++++++++++++++++-------------------- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/App.css b/src/App.css index 71cc3c2f..07ab3f77 100644 --- a/src/App.css +++ b/src/App.css @@ -7,6 +7,10 @@ html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons { height: 100%; } +.clickable-button:hover { + cursor: pointer; +} + body { background-color: #f1f1f1; margin: 0; diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index b0f2599f..2ae2de34 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -52,10 +52,10 @@ export default function StopWatch() {

stopwatchify

- setTimerOn(true)}> - setTimerOn(false)}> - setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes}> - + setTimerOn(true)} className={'clickable-button'}> + setTimerOn(false)} className={'clickable-button'}> + setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes} className={'clickable-button'}> +

{formatTime(time)}

diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx index 7f12a5f1..5f09d585 100644 --- a/src/StopWatchButton.tsx +++ b/src/StopWatchButton.tsx @@ -1,54 +1,56 @@ -import React from 'react' +import React from 'react'; // Maximum number of laps that can be recorded const maxLaps = 25; // Define the props for the StopWatchButton component type StopWatchButtonProps = { - type: 'start' | 'stop' | 'lap' | 'reset'; - onClick?: () => void; - timerOn?: boolean; - time?: number; - lapTimes?: number[]; + type: 'start' | 'stop' | 'lap' | 'reset'; + onClick?: () => void; + timerOn?: boolean; + time?: number; + lapTimes?: number[]; + className?: string; }; - - 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( - - ) + +export default function StopWatchButton({ type, onClick, timerOn, time, lapTimes, className }: 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 ( + + ); } \ No newline at end of file From 5229e23cb448b7d45199fc31a2ca26d8035d1d82 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 23:29:02 -0500 Subject: [PATCH 6/8] Change button focus colour, lap text size --- src/App.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.css b/src/App.css index 07ab3f77..53da74c2 100644 --- a/src/App.css +++ b/src/App.css @@ -39,7 +39,7 @@ body { .stopwatch-buttons button:focus { outline: none; - border: 1px solid black; + border: 1px solid var(--shopify-dark-green); } .stopwatch-buttons button { @@ -77,5 +77,5 @@ body { .stopwatch-laptimes li { padding: 0.75em 0; border-bottom: 1px solid #ebebeb; - font-size: x-large; + font-size: 1.75rem; } \ No newline at end of file From ae01215fa372ec1c05318af2dbb19b3a3ecbcd49 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 23:39:38 -0500 Subject: [PATCH 7/8] Update to show lap times in reverse-chrono order --- src/StopWatch.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 2ae2de34..fd28ce1d 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -62,10 +62,10 @@ export default function StopWatch() { {/* Display the numbered lap times */} {lapTimes.length > 0 && (
-

Lap times

+

Lap Times

    - {lapTimes.map((lapTime, index) => { - return
  • {(index + 1)+'.'} {formatTime(lapTime)}
  • + {lapTimes.slice().reverse().map((lapTime, index) => { + return
  • {(lapTimes.length - index)+'.'} {formatTime(lapTime)}
  • })}
From 209d94b7a44a27e482ad6a0b267b34e447dd36d3 Mon Sep 17 00:00:00 2001 From: Wilson Chu Date: Mon, 29 Jan 2024 23:48:32 -0500 Subject: [PATCH 8/8] Show time difference between each lap and time standings --- src/StopWatch.tsx | 140 +++++++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 63 deletions(-) diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index fd28ce1d..5c6d2f53 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,77 +1,91 @@ -import React, { useState, useEffect, useCallback } from 'react' -import StopWatchButton from './StopWatchButton' +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; + // 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([]); + // 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([]); + + // 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([]); + }, []); - // 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 | null = null; - // 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 | null = null; + if (timerOn) { + interval = setInterval(() => setTime(time => time + 1), 10); + } - if (timerOn) { - interval = setInterval(() => setTime(time => time + 1), 10) - } + return () => { clearInterval(interval); }; // Clears the interval when the component unmounts or timerOn changes + }, [timerOn]); - return () => {clearInterval(interval)} // Clears the interval when the component unmounts or timerOn changes - }, [timerOn]) + return ( +
+

stopwatchify

+
+
+ setTimerOn(true)} className={'clickable-button'}> + setTimerOn(false)} className={'clickable-button'}> + setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes} className={'clickable-button'}> + +
+
+

{formatTime(time)}

+ {/* Display the numbered lap times */} + {lapTimes.length > 0 && ( +
+

Lap Times

+
    - return( -
    -

    stopwatchify

    -
    -
    - setTimerOn(true)} className={'clickable-button'}> - setTimerOn(false)} className={'clickable-button'}> - setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes} className={'clickable-button'}> - -
    -
    -

    {formatTime(time)}

    - {/* Display the numbered lap times */} - {lapTimes.length > 0 && ( -
    -

    Lap Times

    -
      - {lapTimes.slice().reverse().map((lapTime, index) => { - return
    • {(lapTimes.length - index)+'.'} {formatTime(lapTime)}
    • - })} -
    -
    - )} -
    +
  • + Total Time: {formatTime(time)} +
  • + {lapTimes.slice().reverse().map((lapTime, index) => { + const lapDifference = index === 0 ? 0 : lapTimes[index - 1] - lapTime; + return ( +
  • + Lap {lapTimes.length - index}: {formatTime(lapTime)} + {index > 0 && ( + + ({lapDifference >= 0 ? '+' : '-'}{formatTime(Math.abs(lapDifference))}) + + )} +
  • + ); + })} +
+ )}
- ) +
+
+ ); } \ No newline at end of file