Skip to content

Commit

Permalink
Merge branch 'staging' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
govindup63 authored Dec 20, 2024
2 parents 8d38b1e + 18725ad commit a2d998d
Show file tree
Hide file tree
Showing 14 changed files with 1,311 additions and 150 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/schedule-leaderboard-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Update Leaderboard Every Friday

on:
schedule:
- cron: "0 19 * * 5"

jobs:
update-leaderboard:
runs-on: ubuntu-latest

steps:
- name: Trigger API to Update Leaderboard
run: |
response=$(curl -X POST -s -w "%{http_code}" "https://pointblank.club/api/hustle")
if [ "${response: -3}" != "200" ]; then
echo "API call failed with status: ${response: -3}"
exit 1
fi
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ next-env.d.ts

# env
.env
.env.local
.env.local

# include package-lock.json
!/package-lock.json
2 changes: 1 addition & 1 deletion Firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ const db = getFirestore(app);
const googleProvider = new GoogleAuthProvider();
const storage = getStorage(app);

export { auth, googleProvider, db, storage };
export { auth, googleProvider, db, storage, app };
144 changes: 144 additions & 0 deletions app/(default)/api/hustle/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { NextResponse } from "next/server";
import axios from "axios";
// import { JSDOM } from "jsdom";
import puppeteer from "puppeteer";

import {
getFirestore,
doc,
setDoc,
getDoc,
} from "firebase/firestore";
import { app } from "@/Firebase";

interface ContestRanking {
rank: number;
name: string;
score: number;
}

interface LeaderboardUser {
name: string;
score: number;
consistency: number;
rank?: number;
}

interface LeaderboardData {
rankings?: LeaderboardUser[];
updatedAt?: Date;
lastContestCode?: string;
}

export async function POST() {
try {
const db = getFirestore(app);

const API_URL =
process.env.VJUDGE_CONTEST_API ||
"https://vjudge.net/contest/data?draw=2&start=0&length=20&sortDir=desc&sortCol=0&category=mine&running=3&title=&owner=Pbhustle&_=1733642420751";

const { data } = await axios.get(API_URL);
const ccode = data.data[0][0];

const url = `https://vjudge.net/contest/${ccode}#rank`;

const leaderboardRef = doc(db, "hustle", "leaderboard");
const leaderboardDoc = await getDoc(leaderboardRef);

const existingData = leaderboardDoc.data() as LeaderboardData | undefined;
const lastContestCode = existingData?.lastContestCode;

if (lastContestCode === ccode) {
console.log("This contest has already been processed. Skipping update.");
return NextResponse.json({
message: "Leaderboard is already up-to-date.",
});
}

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto(url, { waitUntil: "networkidle2" });

await page.waitForSelector("#contest-rank-table tbody");

const latest: ContestRanking[] = await page.evaluate(() => {
const rows = Array.from(
document.querySelectorAll("#contest-rank-table tbody tr")
);
return rows.slice(0).map((row) => {
const cells = row.querySelectorAll("td");
return {
rank: parseInt(cells[0]?.textContent?.trim() || "0"),
name: cells[1]?.textContent?.trim() || "",
score: parseInt(cells[2]?.textContent?.trim() || "0"),
};
});
});

await browser.close();

const latestRef = doc(db, "hustle", "latest");
await setDoc(latestRef, {
results: latest,
updateTime: new Date(),
});

let rankings: LeaderboardUser[] = existingData?.rankings || [];

latest.forEach(({ name, score }) => {
const existingUser = rankings.find((user) => user.name === name);
if (existingUser) {
existingUser.score += score;
existingUser.consistency += 1;
} else {
rankings.push({ name, score, consistency: 1 });
}
});

rankings.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.consistency !== a.consistency) return b.consistency - a.consistency;
return (a.rank || 0) - (b.rank || 0);
});

rankings.forEach((user, index) => {
user.rank = index + 1;
});

await setDoc(leaderboardRef, {
rankings,
updatedAt: new Date(),
lastContestCode: ccode,
});

return NextResponse.json({
message: "Leaderboard updated successfully",
rankings,
});
} catch (error) {
console.error("Error updating leaderboard:", error);
return NextResponse.json({ error: "Failed to update leaderboard" });
}
}

export async function GET() {
try {
const db = getFirestore();

const latestDoc = await getDoc(doc(db, "hustle", "latest"));
const leaderboardDoc = await getDoc(doc(db, "hustle", "leaderboard"));

return NextResponse.json({
message: "Fetched hustle data successfully",
data: {
latest: latestDoc.data(),
leaderboard: leaderboardDoc.data(),
},
});
} catch (error) {
console.error("Error fetching hustle data:", error);
return NextResponse.json({ error: "Failed to fetch hustle data" });
}
}
209 changes: 209 additions & 0 deletions app/(default)/hustle/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"use client";

import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Trophy, RefreshCw, TableProperties } from "lucide-react";
import { auth } from "../../../Firebase"; // Firebase setup
import { onAuthStateChanged } from "firebase/auth";

interface Result {
rank: number;
name: string;
score: number;
consistency?: number[]; // For Rankings only
}

export default function ResultsTable() {
const [tab, setTab] = useState<"latest" | "rankings">("latest");
const [isAdmin, setIsAdmin] = useState(false);
const [latestResults, setLatestResults] = useState<Result[]>([]);
const [rankings, setRankings] = useState<Result[]>([]);
const [loading, setLoading] = useState(true);
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("/api/hustle");
const data = await response.json();

if (response.ok && data?.data) {
const allDocs = data.data;
const latestData: Result[] = allDocs.latest.results || [];
const rankingsData: Result[] = allDocs.leaderboard.rankings || [];

setLatestResults(latestData);
setRankings(rankingsData);
} else {
console.error("Error in response:", data.error);
}

setLoading(false);
} catch (error) {
console.error("Failed to fetch results:", error);
setLoading(false);
}
};

const checkAdmin = async (uid: string) => {
try {
const response = await fetch(`/api/admin?uid=${uid}`);
const { isAdmin } = await response.json();
setIsAdmin(isAdmin);
setIsAdminLoggedIn(true);
} catch (error) {
console.error("Error checking admin status:", error);
setIsAdmin(false);
setIsAdminLoggedIn(false);
}
};

const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
checkAdmin(user.uid);
} else {
setIsAdmin(false);
setIsAdminLoggedIn(false);
}
});

fetchData();

return () => unsubscribe();
}, []);

const handleGetResults = async () => {
try {
setLoading(true);

const res = await fetch("/api/hustle", {
method: "POST", // Specify the request method
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key: "value" }),
});

if (res.ok) {
alert("Scraper executed successfully!");
} else {
alert("Failed to update results.");
}
setLoading(false);
} catch (error) {
console.error("Error triggering scraper:", error);
setLoading(false);
}
};

// const renderConsistency = (consistency?: number[]) => {
// if (!consistency) return "N/A";

// return consistency.map((value, index) => (
// <span
// key={index}
// className="mx-1"
// style={{ color: value === 1 ? "#00C853" : "red" }}
// >
// {value === 1 ? "✓" : "✗"}
// </span>
// ));
// };

const renderTable = (data: Result[], showConsistency: boolean) => (
<div className="overflow-x-auto shadow-xl rounded-xl border border-gray-700">
<table className="w-full text-sm text-left text-gray-300">
<thead className="text-xs uppercase bg-gray-800 text-[#00FF66]">
<tr>
<th className="px-6 py-4">Rank</th>
<th className="px-6 py-4">Name</th>
<th className="px-6 py-4">Score</th>
</tr>
</thead>
<tbody>
{data.map((result, index) => (
<tr
key={index}
className="border-b bg-gray-900 border-gray-700 hover:bg-gray-800 transition-colors"
>
<td className="px-6 py-4 font-medium whitespace-nowrap text-[#00FF66]">
{result.rank}
</td>
<td className="px-6 py-4">{result.name}</td>
<td className="px-6 py-4 font-semibold">{result.score}</td>
</tr>
))}
</tbody>
</table>
</div>
);

return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="max-w-5xl mx-auto pt-14">
<motion.header
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<h1 className="text-5xl font-bold mb-6 text-[#00FF66] flex items-center justify-center gap-4">
<Trophy className="w-12 h-12" /> PB HUSTLE
</h1>
<p className="text-gray-300 max-w-2xl mx-auto text-lg">
Track latest results and overall rankings in real-time
</p>
</motion.header>


<div className="flex justify-center space-x-6 mb-8">
{(["latest", "rankings"] as const).map((tabName) => (
<button
key={tabName}
onClick={() => setTab(tabName)}
className={`
px-6 py-3 rounded-full flex items-center gap-3 transition-all text-lg font-semibold
${
tab === tabName
? "bg-[#00FF66] text-gray-900 shadow-lg"
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
}
`}
>
<TableProperties className="w-6 h-6" />
{tabName === "latest" ? "Latest Results" : "Rankings"}
</button>
))}
</div>
{isAdmin && isAdminLoggedIn && tab === "latest" && (
<div className="flex justify-center mb-4">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleGetResults}
disabled={loading}
className="
bg-[#00FF66] text-gray-900 px-8 py-3 rounded-full
flex items-center gap-3 shadow-lg hover:bg-opacity-90 text-lg font-semibold
disabled:opacity-50 disabled:cursor-not-allowed
"
>
<RefreshCw className="w-6 h-6" />
{loading ? "Updating..." : "GET LATEST RESULTS"}
</motion.button>
</div>
)}

{loading ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-[#00FF66]"></div>
</div>
) : (
renderTable(
tab === "latest" ? latestResults : rankings,
tab === "rankings"
)
)}
</div>
</div>
);
}
Loading

0 comments on commit a2d998d

Please sign in to comment.