Skip to content

Commit dc6f4c5

Browse files
committed
notification and timer
1 parent 5193d41 commit dc6f4c5

File tree

7 files changed

+1167
-12
lines changed

7 files changed

+1167
-12
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"axios": "^1.7.7",
3232
"class-variance-authority": "^0.7.0",
3333
"clsx": "^2.1.1",
34+
"firebase": "^12.3.0",
3435
"geist": "^1.3.0",
3536
"lucide-react": "^0.437.0",
3637
"next": "^14.2.4",

pnpm-lock.yaml

Lines changed: 777 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/timer.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { handleAPIError } from "@/lib/error";
2+
import api from ".";
3+
4+
export interface GetTimeResponse {
5+
server_time: string;
6+
round_start_time: string;
7+
round_end_time: string;
8+
}
9+
10+
export interface SetTimeParams {
11+
round: string;
12+
time: string;
13+
}
14+
15+
export interface UpdateTimeParams {
16+
duration: number;
17+
}
18+
19+
export interface StartRoundResponse {
20+
success: boolean;
21+
round: number;
22+
}
23+
24+
/**
25+
* Fetch the current round and server times.
26+
*/
27+
export async function getTime(): Promise<GetTimeResponse | null> {
28+
try {
29+
const response = await api.get<GetTimeResponse>("/GetTime");
30+
return response.data;
31+
} catch (e) {
32+
console.error(e);
33+
return null;
34+
}
35+
}
36+
37+
/**
38+
* Set the end time for a specific round.
39+
*/
40+
export async function setTime(
41+
data: SetTimeParams,
42+
): Promise<{ success: boolean }> {
43+
try {
44+
const response = await api.post<{ success: boolean }>("/SetTime", data);
45+
return response.data;
46+
} catch (e) {
47+
throw handleAPIError(e);
48+
}
49+
}
50+
51+
/**
52+
* Update the remaining time of the current round.
53+
*/
54+
export async function updateTime(data: UpdateTimeParams): Promise<void> {
55+
try {
56+
await api.post("/UpdateTime", data);
57+
} catch (e) {
58+
throw handleAPIError(e);
59+
}
60+
}
61+
62+
/**
63+
* Start a new round. Increments internal round counter and sets start times.
64+
*/
65+
export async function startRound(): Promise<StartRoundResponse> {
66+
try {
67+
const response = await api.post<StartRoundResponse>("/StartRound");
68+
return response.data;
69+
} catch (e) {
70+
throw handleAPIError(e);
71+
}
72+
}

src/app/dashboard/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22
import { getLeaderboard, type LeaderboardUser } from "@/api/users";
3+
import NotificationsSender from "@/components/NotificationsSender";
34
import Round from "@/components/round";
45
import { useQuery } from "@tanstack/react-query";
56

@@ -21,6 +22,13 @@ function Dashboard() {
2122
<Round />
2223
</div>
2324

25+
<div className="s-sling m-3 mt-10 text-center text-xl font-semibold">
26+
Notifications
27+
</div>
28+
<div className="flex w-full justify-center text-black">
29+
<NotificationsSender />
30+
</div>
31+
2432
<div className="s-sling m-3 mt-10 text-center text-xl font-semibold">
2533
Leaderboard
2634
</div>

src/app/timer/page.tsx

Lines changed: 240 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,247 @@
11
"use client";
22

3+
import {
4+
getTime,
5+
GetTimeResponse,
6+
setTime,
7+
SetTimeParams,
8+
startRound,
9+
updateTime,
10+
UpdateTimeParams,
11+
} from "@/api/timer";
12+
import { useEffect, useState } from "react";
13+
import { FaPlay, FaPlus, FaStopCircle } from "react-icons/fa";
14+
315
function Timer() {
16+
const [selectedRound, setSelectedRound] = useState("1");
17+
const [isRunning, setIsRunning] = useState(false);
18+
const [isStarting, setIsStarting] = useState(false);
19+
const [timerData, setTimerData] = useState<GetTimeResponse | null>(null);
20+
const [remainingSeconds, setRemainingSeconds] = useState(0);
21+
const [duration, setDuration] = useState({
22+
hours: 0,
23+
minutes: 0,
24+
seconds: 0,
25+
});
26+
const [addTimeValue, setAddTimeValue] = useState({
27+
hours: 0,
28+
minutes: 0,
29+
seconds: 0,
30+
});
31+
32+
async function fetchTimer() {
33+
const data = await getTime();
34+
if (data) {
35+
setTimerData(data);
36+
const now = new Date(data.server_time).getTime();
37+
const end = new Date(data.round_end_time).getTime();
38+
const diff = Math.max(0, Math.floor((end - now) / 1000));
39+
setRemainingSeconds(diff);
40+
}
41+
}
42+
43+
useEffect(() => {
44+
let tick: NodeJS.Timer;
45+
if (isRunning) {
46+
tick = setInterval(() => {
47+
setRemainingSeconds((prev) => Math.max(0, prev - 1));
48+
}, 1000);
49+
}
50+
return () => clearInterval(tick);
51+
}, [isRunning]);
52+
53+
useEffect(() => {
54+
let sync: NodeJS.Timer;
55+
if (isRunning) {
56+
void fetchTimer();
57+
sync = setInterval(() => {
58+
void fetchTimer();
59+
}, 120_000);
60+
}
61+
return () => clearInterval(sync);
62+
}, [isRunning]);
63+
64+
const getDurationInSeconds = (d: typeof duration) =>
65+
d.hours * 3600 + d.minutes * 60 + d.seconds;
66+
67+
async function handleSetTime() {
68+
const seconds = getDurationInSeconds(duration);
69+
const payload: SetTimeParams = {
70+
round: selectedRound,
71+
time: seconds.toString(),
72+
};
73+
await setTime(payload);
74+
void fetchTimer();
75+
}
76+
77+
async function handleStartRound() {
78+
setIsStarting(true);
79+
await startRound();
80+
setIsRunning(true);
81+
setIsStarting(false);
82+
void fetchTimer();
83+
}
84+
85+
async function handleAddTime() {
86+
const seconds =
87+
addTimeValue.hours * 3600 +
88+
addTimeValue.minutes * 60 +
89+
addTimeValue.seconds;
90+
const payload: UpdateTimeParams = { duration: seconds };
91+
await updateTime(payload);
92+
void fetchTimer();
93+
setAddTimeValue({ hours: 0, minutes: 0, seconds: 0 });
94+
}
95+
96+
function formatTime(sec: number) {
97+
const h = Math.floor(sec / 3600)
98+
.toString()
99+
.padStart(2, "0");
100+
const m = Math.floor((sec % 3600) / 60)
101+
.toString()
102+
.padStart(2, "0");
103+
const s = (sec % 60).toString().padStart(2, "0");
104+
return `${h}:${m}:${s}`;
105+
}
106+
4107
return (
5-
<div className="min-h-screen text-white">
6-
<h1 className="text-2xl font-bold">Timer</h1>
7-
<p>Hi from Timer</p>
108+
<div className="flex min-h-screen flex-col items-center justify-center p-4 text-white">
109+
<h1 className="mb-8 text-3xl font-bold">Timer</h1>
110+
111+
<div className="flex w-full max-w-lg flex-col items-center gap-6 rounded-xl bg-gray-800 p-8">
112+
{/* Round Selection + Duration */}
113+
<div className="flex w-full items-center gap-3">
114+
<select
115+
className="w-28 rounded-lg bg-gray-700 p-2 text-white"
116+
value={selectedRound}
117+
onChange={(e) => setSelectedRound(e.target.value)}
118+
>
119+
<option value="1">Round 1</option>
120+
<option value="2">Round 2</option>
121+
<option value="3">Round 3</option>
122+
</select>
123+
124+
{/* Duration Inputs */}
125+
<div className="flex items-center gap-1">
126+
<input
127+
type="number"
128+
min={0}
129+
placeholder="HH"
130+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
131+
value={duration.hours}
132+
onChange={(e) =>
133+
setDuration({ ...duration, hours: Number(e.target.value) })
134+
}
135+
/>
136+
<span>:</span>
137+
<input
138+
type="number"
139+
min={0}
140+
placeholder="MM"
141+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
142+
value={duration.minutes}
143+
onChange={(e) =>
144+
setDuration({ ...duration, minutes: Number(e.target.value) })
145+
}
146+
/>
147+
<span>:</span>
148+
<input
149+
type="number"
150+
min={0}
151+
placeholder="SS"
152+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
153+
value={duration.seconds}
154+
onChange={(e) =>
155+
setDuration({ ...duration, seconds: Number(e.target.value) })
156+
}
157+
/>
158+
</div>
159+
160+
<button
161+
className="ml-2 whitespace-nowrap rounded-lg bg-blue-600 px-4 py-1 text-center text-white transition hover:bg-blue-500"
162+
onClick={handleSetTime}
163+
>
164+
Set Time
165+
</button>
166+
</div>
167+
168+
{/* Timer Display + Start/Stop */}
169+
<div className="flex w-full items-center justify-between gap-4">
170+
<div className="font-mono text-4xl">
171+
{formatTime(remainingSeconds)}
172+
</div>
173+
<button
174+
disabled={isStarting}
175+
className={`rounded-lg px-4 py-2 transition ${
176+
isStarting
177+
? "cursor-not-allowed border-gray-500 bg-gray-500"
178+
: isRunning
179+
? "border-red-600 bg-red-600 hover:bg-red-500"
180+
: "border-green-500 bg-green-500 hover:bg-green-400"
181+
}`}
182+
onClick={handleStartRound}
183+
>
184+
{isStarting ? (
185+
"Starting..."
186+
) : isRunning ? (
187+
<FaStopCircle size={20} />
188+
) : (
189+
<FaPlay size={20} />
190+
)}
191+
</button>
192+
</div>
193+
194+
{/* Add Time Section */}
195+
<div className="flex w-full items-center justify-center gap-2">
196+
<input
197+
type="number"
198+
min={0}
199+
placeholder="HH"
200+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
201+
value={addTimeValue.hours}
202+
onChange={(e) =>
203+
setAddTimeValue({
204+
...addTimeValue,
205+
hours: Number(e.target.value),
206+
})
207+
}
208+
/>
209+
<span>:</span>
210+
<input
211+
type="number"
212+
min={0}
213+
placeholder="MM"
214+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
215+
value={addTimeValue.minutes}
216+
onChange={(e) =>
217+
setAddTimeValue({
218+
...addTimeValue,
219+
minutes: Number(e.target.value),
220+
})
221+
}
222+
/>
223+
<span>:</span>
224+
<input
225+
type="number"
226+
min={0}
227+
placeholder="SS"
228+
className="w-16 rounded-lg bg-gray-700 p-2 text-center font-mono text-white"
229+
value={addTimeValue.seconds}
230+
onChange={(e) =>
231+
setAddTimeValue({
232+
...addTimeValue,
233+
seconds: Number(e.target.value),
234+
})
235+
}
236+
/>
237+
<button
238+
className="flex items-center justify-center rounded-lg bg-gray-700 p-3 hover:bg-gray-600"
239+
onClick={handleAddTime}
240+
>
241+
<FaPlus />
242+
</button>
243+
</div>
244+
</div>
8245
</div>
9246
);
10247
}

0 commit comments

Comments
 (0)