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
diff --git a/src/App.css b/src/App.css index e5deef95..53da74c2 100644 --- a/src/App.css +++ b/src/App.css @@ -1,18 +1,28 @@ +:root { + --shopify-green: #96bf48; + --shopify-dark-green: #769241; +} + html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons { - height: -webkit-fill-available; + height: 100%; +} + +.clickable-button:hover { + cursor: pointer; } body { background-color: #f1f1f1; - margin: 0px; + margin: 0; } .stopwatch-title { - background-color: #303030; - 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 { @@ -23,49 +33,49 @@ 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 var(--shopify-dark-green); } .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%; - 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 { 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; + font-size: 1.75rem; } \ No newline at end of file diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 2e06473c..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( -
    -

    StopWatch

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

    {formatTime(time)}

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

    Lap times

    -
      - {lapTimes.map((lapTime, index) => { - return
    • {(index + 1)+'.'} {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 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