Skip to content

Commit 93e4955

Browse files
committed
feat: stopwatch
1 parent f8e0a1d commit 93e4955

File tree

5 files changed

+90
-10
lines changed

5 files changed

+90
-10
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,13 @@
252252
"title": "CRON Job Generator",
253253
"description": "Generate CRON expressions visually and convert human-readable text to CRON.",
254254
"icon": "ClockIcon"
255+
},
256+
{
257+
"slug": "stopwatch",
258+
"to": "/apps/stopwatch",
259+
"title": "Stopwatch",
260+
"description": "A simple stopwatch with lap functionality.",
261+
"icon": "TimerIcon"
255262
}
256263
]
257264
}

src/components/AnimatedRoutes.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import RockPaperScissorsPage from '../pages/apps/RockPaperScissorsPage'; // Impo
5151
import TicTacToePage from '../pages/apps/TicTacToePage'; // Import TicTacToePage
5252
import ConnectFourPage from '../pages/apps/ConnectFourPage'; // Import ConnectFourPage
5353
import ImageCompressorPage from '../pages/apps/ImageCompressorPage'; // Import ImageCompressorPage
54+
import StopwatchAppPage from '../pages/StopwatchAppPage'; // Import StopwatchAppPage
5455
import SettingsPage from '../pages/SettingsPage';
5556

5657
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -988,6 +989,20 @@ function AnimatedRoutes() {
988989
</motion.div>
989990
}
990991
/>
992+
<Route
993+
path="/apps/stopwatch"
994+
element={
995+
<motion.div
996+
initial="initial"
997+
animate="in"
998+
exit="out"
999+
variants={pageVariants}
1000+
transition={pageTransition}
1001+
>
1002+
<StopwatchAppPage />
1003+
</motion.div>
1004+
}
1005+
/>
9911006
{/* D&D specific 404 page */}
9921007
<Route
9931008
path="/stories/*"

src/components/Stopwatch.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import React, { useState, useEffect, useRef } from 'react';
22

33
const Stopwatch = () => {
44
const [time, setTime] = useState(0);
5+
const [laps, setLaps] = useState([]);
56
const [isRunning, setIsRunning] = useState(false);
67
const intervalRef = useRef(null);
78

89
useEffect(() => {
910
if (isRunning) {
1011
intervalRef.current = setInterval(() => {
11-
setTime(prevTime => prevTime + 10); // Update every 10ms for smoother display
12+
setTime(prevTime => prevTime + 10);
1213
}, 10);
1314
} else {
1415
clearInterval(intervalRef.current);
@@ -20,33 +21,36 @@ const Stopwatch = () => {
2021
const handleStart = () => {
2122
setIsRunning(true);
2223
};
23-
2424
const handleStop = () => {
2525
setIsRunning(false);
2626
};
27-
2827
const handleReset = () => {
2928
setIsRunning(false);
3029
setTime(0);
30+
setLaps([]);
31+
};
32+
const handleLap = () => {
33+
setLaps(prevLaps => [...prevLaps, time]);
3134
};
3235

33-
const formatTime = () => {
34-
const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
35-
const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
36-
const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
37-
const hours = `0${Math.floor(time / 3600000)}`.slice(-2);
36+
const formatTime = (timeValue) => {
37+
const milliseconds = `0${(timeValue % 1000) / 10}`.slice(-2);
38+
const seconds = `0${Math.floor(timeValue / 1000) % 60}`.slice(-2);
39+
const minutes = `0${Math.floor(timeValue / 60000) % 60}`.slice(-2);
40+
const hours = `0${Math.floor(timeValue / 3600000)}`.slice(-2);
3841
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
3942
};
4043

4144
const buttonStyle = "w-24 px-4 py-2 text-lg font-arvo rounded-md border transition-colors duration-300 ease-in-out";
4245
const activeButtonStyle = "bg-green-800/50 border-green-700 text-white hover:bg-green-700/50";
4346
const inactiveButtonStyle = "bg-gray-700/50 border-gray-600 text-gray-300 hover:bg-gray-600/50";
4447
const stopButtonStyle = "bg-red-800/50 border-red-700 text-white hover:bg-red-700/50";
48+
const lapButtonStyle = "bg-blue-800/50 border-blue-700 text-white hover:bg-blue-700/50 disabled:opacity-50 disabled:cursor-not-allowed";
4549

4650
return (
47-
<div className="flex flex-col items-center gap-6">
51+
<div className="flex flex-col items-center gap-6 w-full max-w-md">
4852
<div className="font-mono text-5xl text-gray-100 p-4 rounded-lg bg-gray-900/50 w-full text-center">
49-
{formatTime()}
53+
{formatTime(time)}
5054
</div>
5155
<div className="flex gap-4">
5256
{!isRunning ? (
@@ -58,10 +62,26 @@ const Stopwatch = () => {
5862
Stop
5963
</button>
6064
)}
65+
<button onClick={handleLap} className={`${buttonStyle} ${lapButtonStyle}`} disabled={!isRunning}>
66+
Lap
67+
</button>
6168
<button onClick={handleReset} className={`${buttonStyle} ${inactiveButtonStyle}`}>
6269
Reset
6370
</button>
6471
</div>
72+
{laps.length > 0 && (
73+
<div className="w-full mt-4 p-4 rounded-lg bg-gray-900/50">
74+
<h3 className="text-xl font-arvo text-center text-gray-200 mb-2">Laps</h3>
75+
<ul className="space-y-2 max-h-60 overflow-y-auto">
76+
{laps.map((lapTime, index) => (
77+
<li key={index} className="flex justify-between font-mono text-gray-300 p-2 bg-gray-800/50 rounded">
78+
<span>Lap {index + 1}</span>
79+
<span>{formatTime(lapTime)}</span>
80+
</li>
81+
)).reverse()}
82+
</ul>
83+
</div>
84+
)}
6585
</div>
6686
);
6787
};

src/pages/StopwatchAppPage.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import Stopwatch from '../components/Stopwatch';
3+
import useSeo from '../hooks/useSeo';
4+
import { Link } from 'react-router-dom';
5+
import { ArrowLeft } from '@phosphor-icons/react';
6+
7+
const StopwatchAppPage = () => {
8+
useSeo({
9+
title: 'Stopwatch App | Fezcodex',
10+
description: 'A simple and clean stopwatch with lap functionality.',
11+
keywords: ['stopwatch', 'timer', 'utility', 'time', 'lap timer'],
12+
});
13+
14+
return (
15+
<div className="py-16 sm:py-24 flex flex-col items-center justify-center">
16+
<Link
17+
to="/apps"
18+
className="text-primary-400 hover:underline flex items-center justify-center gap-2 text-lg mt-12"
19+
>
20+
<ArrowLeft size={24} /> Back to Apps
21+
</Link>
22+
<div className="text-center mb-12">
23+
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-6xl">
24+
Stopwatch
25+
</h1>
26+
<p className="mt-6 text-lg leading-8 text-gray-300">
27+
A simple utility to measure time and record laps.
28+
</p>
29+
</div>
30+
<Stopwatch />
31+
32+
</div>
33+
);
34+
};
35+
36+
export default StopwatchAppPage;

src/utils/appIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
HandshakeIcon,
3131
XCircleIcon,
3232
ArrowsInLineHorizontalIcon,
33+
TimerIcon,
3334
} from '@phosphor-icons/react';
3435

3536
export const appIcons = {
@@ -64,4 +65,5 @@ export const appIcons = {
6465
HandshakeIcon,
6566
XCircleIcon,
6667
ArrowsInLineHorizontalIcon,
68+
TimerIcon,
6769
};

0 commit comments

Comments
 (0)