Skip to content

Commit b116346

Browse files
chore: merge dev to main for deployment (#10)
* fixing sorting logic * saving * refactor: changing code structure * feat: good sorting ui * feat: load actual challenge data * feat: CORE functionality implemented * feat: cleaned the directory * feat: cleaned dir * feat: landing page * Feat/sparkrush UI (#3) * made the light mode on spark rush * feat: score overlay * feat: countdown * Feat/sparkrush UI (#5) * made the light mode on spark rush * feat: score overlay * feat: countdown * feat: start countdown * feat: start countdown * feat(landing): enhanced overall landing (#8) * release v0.3 (#7) * fixing sorting logic * saving * refactor: changing code structure * feat: good sorting ui * feat: load actual challenge data * feat: CORE functionality implemented * feat: cleaned the directory * feat: cleaned dir * feat: landing page * Feat/sparkrush UI (#3) * made the light mode on spark rush * feat: score overlay * feat: countdown * Feat/sparkrush UI (#5) * made the light mode on spark rush * feat: score overlay * feat: countdown * feat: start countdown * feat: start countdown * feat(landing): enhanced overall landing page experience --------- Co-authored-by: Erwin <111296942+SauceCode01@users.noreply.github.com> * feat: added lucide react in package.json * Feat/leaderboards (#9) * feat: install firebase * feat: firebase setup * feat: created the api endpoints * feat: fixed leaderboards api * feat: fix game loop and timer at the start * feat: better ui design for countdown * feat: better loading design * feat: added date on leaderboards * fix: build error --------- Co-authored-by: SauceCode01 <sauce.code.01@gmail.com> Co-authored-by: Erwin <111296942+SauceCode01@users.noreply.github.com>
1 parent af1a632 commit b116346

15 files changed

Lines changed: 3274 additions & 145 deletions

File tree

package-lock.json

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

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"@dnd-kit/core": "^6.3.1",
1313
"@dnd-kit/modifiers": "^9.0.0",
1414
"@dnd-kit/sortable": "^10.0.0",
15+
"firebase": "^12.6.0",
16+
"firebase-admin": "^13.6.0",
17+
"framer-motion": "^12.23.25",
18+
"lucide-react": "^0.556.0",
1519
"next": "16.0.7",
1620
"react": "19.2.0",
1721
"react-dom": "19.2.0"

src/app/api/leaderboards/route.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { adminDb } from "@/lib/firebaseAdmin";
2+
import {
3+
CreateLeaderboardEntryDTO,
4+
Leaderboard,
5+
} from "@/types/leaderboardTypes";
6+
import { FieldValue } from "firebase-admin/firestore";
7+
import { NextRequest, NextResponse } from "next/server";
8+
9+
export async function POST(req: NextRequest) {
10+
try {
11+
const leaderboardEntryDTO = (await req.json()) as CreateLeaderboardEntryDTO;
12+
13+
const ref = adminDb.collection("leaderboards").doc("leaderboard");
14+
15+
const newEntry = {
16+
...leaderboardEntryDTO,
17+
date: Date.now(),
18+
id: crypto.randomUUID(),
19+
};
20+
21+
await ref.update({
22+
entries: FieldValue.arrayUnion(newEntry),
23+
});
24+
25+
return NextResponse.json(
26+
{ message: "Entry created successfully" },
27+
{ status: 200 }
28+
);
29+
} catch (err: unknown) {
30+
return NextResponse.json(
31+
{ error: "Failed to create message" },
32+
{ status: 500 }
33+
);
34+
}
35+
}
36+
37+
export async function GET(req: NextRequest) {
38+
try {
39+
// get current leaderboard
40+
const ref = adminDb.collection("leaderboards").doc("leaderboard");
41+
const snap = await ref.get();
42+
43+
let leaderboard: Leaderboard = {
44+
entries: [],
45+
};
46+
47+
if (snap.exists) {
48+
leaderboard = snap.data() as Leaderboard;
49+
} else {
50+
// create new leaderboard in firestore
51+
await ref.set({ entries: [] });
52+
}
53+
54+
const sortedEntries = leaderboard.entries.sort((a, b) => b.score - a.score);
55+
56+
// return response
57+
return NextResponse.json(sortedEntries, { status: 200 });
58+
} catch (err: unknown) {
59+
return NextResponse.json(
60+
{ error: "Failed to get message" },
61+
{ status: 500 }
62+
);
63+
}
64+
}

src/app/leaderboards/page.tsx

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"use client";
2+
3+
import { BouncyShape } from "@/components/ui/BouncyShape";
4+
import { Logo } from "@/components/ui/Logo";
5+
import { db } from "@/lib/firebase";
6+
import { getLeaderboards, postLeaderBoardEntry } from "@/lib/leaderboards";
7+
import { LeaderBoardEntry } from "@/types/leaderboardTypes";
8+
import { doc, getDoc, setDoc } from "firebase/firestore";
9+
import { motion } from "framer-motion";
10+
import { ChevronLeft, Crown, Send, Sparkles, Trophy } from "lucide-react";
11+
import Link from "next/link";
12+
import React, { useEffect, useState } from "react";
13+
14+
const colors = {
15+
blue: "#4285F4",
16+
red: "#EA4335",
17+
yellow: "#FBBC04",
18+
green: "#34A853",
19+
};
20+
21+
export const LeaderboardsPage = () => {
22+
const [leaderboards, setLeaderboards] = useState<LeaderBoardEntry[]>([]);
23+
24+
const handleOnGetLeaderboards = async () => {
25+
const res = await getLeaderboards();
26+
setLeaderboards(res);
27+
};
28+
29+
useEffect(() => {
30+
handleOnGetLeaderboards();
31+
}, []);
32+
33+
return (
34+
<div className="relative flex flex-col min-h-screen bg-white text-gray-900 overflow-x-hidden font-sans">
35+
<div className="gdg-decor-bg" aria-hidden="true">
36+
<span className="bracket text-gray-900" style={{ marginRight: "2rem" }}>
37+
&lt;
38+
</span>
39+
<span className="bracket text-gray-900">&gt;</span>
40+
</div>
41+
42+
{/* HEADER */}
43+
<header className="w-full px-6 md:px-12 py-6 flex justify-between items-center bg-white/60 backdrop-blur-sm sticky top-0 z-50">
44+
<div className="flex items-center gap-3">
45+
<Link href="/">
46+
<div className="w-full transform hover:scale-105 transition-transform duration-100 cursor-pointer">
47+
<Logo />
48+
</div>
49+
</Link>
50+
</div>
51+
<nav>
52+
<Link href="/spark-rush">
53+
<button
54+
className="group px-6 py-2.5 text-white font-medium text-sm rounded-full shadow-md hover:shadow-lg hover:shadow-blue-200 transition-all transform hover:-translate-y-0.5 flex items-center gap-2"
55+
style={{ backgroundColor: colors.blue }}
56+
>
57+
<ChevronLeft
58+
size={16}
59+
className="group-hover:-translate-x-1 transition-transform"
60+
/>
61+
Back to Arena
62+
</button>
63+
</Link>
64+
</nav>
65+
</header>
66+
67+
{/* MAIN CONTENT */}
68+
<main className="flex-grow flex flex-col">
69+
<section className="relative w-full pt-20 pb-32 px-4 flex flex-col items-center justify-center min-h-[80vh]">
70+
{/* === FRAMER MOTION GEOMETRIC SHAPES === */}
71+
<div className="absolute inset-0 pointer-events-none overflow-hidden">
72+
<BouncyShape
73+
color={colors.blue}
74+
className="top-20 left-[10%] w-24 h-24 pointer-events-auto"
75+
initialRotate={12}
76+
delay={0}
77+
/>
78+
<BouncyShape
79+
type="circle"
80+
color={colors.red}
81+
className="bottom-40 right-[10%] w-32 h-32 pointer-events-auto"
82+
initialRotate={0}
83+
delay={2}
84+
/>
85+
<BouncyShape
86+
color={colors.yellow}
87+
className="top-32 right-[20%] w-16 h-16 pointer-events-auto"
88+
initialRotate={45}
89+
delay={1.5}
90+
/>
91+
<motion.div
92+
className="absolute bottom-20 left-[15%] w-40 h-12 pointer-events-auto cursor-pointer"
93+
initial={{ rotate: -12, scale: 1 }}
94+
whileHover={{
95+
scale: 1.1,
96+
rotate: -5,
97+
}}
98+
whileTap={{ scale: 0.95 }}
99+
transition={{
100+
type: "spring",
101+
stiffness: 300,
102+
damping: 15,
103+
}}
104+
>
105+
<motion.div
106+
className="w-full h-full"
107+
animate={{
108+
y: [0, -15, 0],
109+
rotate: [0, 4, -8, 0],
110+
}}
111+
transition={{
112+
duration: 6,
113+
repeat: Infinity,
114+
ease: "easeInOut",
115+
delay: 0.5,
116+
}}
117+
>
118+
<div
119+
className="w-full h-full rounded-full opacity-80 shadow-lg"
120+
style={{ backgroundColor: colors.green }}
121+
/>
122+
</motion.div>
123+
</motion.div>
124+
</div>
125+
126+
<div className="relative z-10 text-center max-w-5xl mx-auto space-y-8">
127+
<div className="inline-flex items-center gap-2 px-5 py-2 rounded-full bg-yellow-50 text-yellow-700 text-sm font-bold tracking-wide animate-fade-in-up border border-yellow-100">
128+
<Trophy size={16} fill="currentColor" />
129+
<span>Leaderboard</span>
130+
</div>
131+
132+
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-[0.9] text-gray-900">
133+
HALL OF FAME
134+
</h1>
135+
136+
<div className="w-full max-w-2xl mx-auto bg-white/50 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
137+
<div className="flex justify-between items-center mb-6">
138+
<h2 className="text-2xl font-bold text-gray-800">Top Players</h2>
139+
<Sparkles size={24} className="text-yellow-500" />
140+
</div>
141+
142+
<div className="space-y-4">
143+
{leaderboards.map((entry, index) => {
144+
const rank = index + 1;
145+
let rankColor = "text-gray-400";
146+
if (rank === 1) rankColor = "text-yellow-400";
147+
if (rank === 2) rankColor = "text-gray-300";
148+
if (rank === 3) rankColor = "text-yellow-600";
149+
150+
return (
151+
<div
152+
key={entry.id}
153+
className="flex items-center justify-between p-4 rounded-xl bg-white shadow-md transition-transform hover:scale-105 border-l-8"
154+
style={{
155+
borderColor:
156+
rank === 1
157+
? colors.yellow
158+
: rank === 2
159+
? colors.red
160+
: rank === 3
161+
? colors.blue
162+
: "transparent",
163+
}}
164+
>
165+
<div className="flex items-center gap-4">
166+
<span
167+
className={`text-3xl font-bold w-10 ${rankColor}`}
168+
>
169+
{rank}
170+
</span>
171+
<div className="flex flex-col items-start">
172+
<span className="text-xl font-semibold text-gray-800">
173+
{entry.username?.trim()}
174+
</span>
175+
<span className="text-xs text-gray-500">
176+
{new Date(entry.date).toLocaleDateString()}
177+
</span>
178+
</div>
179+
</div>
180+
<div className="flex items-center gap-4">
181+
<span
182+
className="text-2xl font-bold"
183+
style={{ color: colors.blue }}
184+
>
185+
{entry.score}
186+
</span>
187+
{rank === 1 && (
188+
<Crown
189+
size={24}
190+
className="text-yellow-400 -rotate-12"
191+
/>
192+
)}
193+
</div>
194+
</div>
195+
);
196+
})}
197+
</div>
198+
</div>
199+
</div>
200+
</section>
201+
</main>
202+
203+
<footer className="w-full py-8 text-center border-t border-gray-100 bg-white relative z-10">
204+
<p className="text-gray-500 font-medium">
205+
Made with <span className="text-red-500"></span> by GDG On Campus
206+
</p>
207+
</footer>
208+
209+
<style jsx global>{`
210+
.gdg-decor-bg {
211+
position: fixed;
212+
top: 0;
213+
left: 0;
214+
width: 100%;
215+
height: 100%;
216+
pointer-events: none;
217+
z-index: 0;
218+
display: flex;
219+
justify-content: center;
220+
align-items: center;
221+
opacity: 0.03;
222+
font-weight: 900;
223+
font-size: 40vw;
224+
user-select: none;
225+
}
226+
227+
.gdg-decor-bg .bracket {
228+
transform: scaleY(0.9);
229+
font-family: sans-serif;
230+
letter-spacing: -2vw;
231+
}
232+
233+
@keyframes fadeInUp {
234+
from {
235+
opacity: 0;
236+
transform: translateY(20px);
237+
}
238+
to {
239+
opacity: 1;
240+
transform: translateY(0);
241+
}
242+
}
243+
.animate-fade-in-up {
244+
animation: fadeInUp 0.6s ease-out forwards;
245+
}
246+
`}</style>
247+
</div>
248+
);
249+
};
250+
251+
252+
export default LeaderboardsPage;

0 commit comments

Comments
 (0)