diff --git a/README.md b/README.md index 8b2275a..bd5f0ef 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,16 @@ ### TODO -- [ ] Handle the color of text inside the problems(make it easy to read) +- [x] Handle the color of text inside the problems(make it easy to read) - [ ] Add img preview for each team to show in popup(use api from domjudge,also update the python script for add img for teams,also we need script to just update the img in teams) - [ ] Add static version(use domjudge shdaow and maybe use nextjs) - [ ] Responsive design(if we deploy in backend service and share with others) -- [ ] Add font for persian and english - [ ] Theme system (Light/Dark/Custom) -- [ ] Add preview +- [ ] Add preview of result - [ ] Add document for functions in scorboard component - [ ] Refactor the scoreboard component - [ ] Docker! +- [ ] New design ?! remove the header add the popup for hover in problems to show the lable of that also make the gap between items ## Lets use it! @@ -52,6 +52,8 @@ node cors-proxy.js & npm run dev ``` +> Keep in mind if you want to share the port of NAT use --host after bun dev or bun preview + ### env file : copy The .env.example to .env: @@ -96,3 +98,17 @@ google-chrome-stable --user-data-dir="./" --disable-web-security ``` For the firefox : About:config and the security.fileuri.strict_origin_policy. Sometimes also the network.http.refere.XOriginPolicy. Im not sure:)) + + + +### Doc +The resolver it self dosen't any really hard method ,Just get the data from domjudge with api and sort the teams +For each resolv the of any submit update the teams and rendere the new array of teams with nice animation ,And yse that's the magic +These days im trying to seprate the functions of scoreboard component ,Because a little bit feel heavy + + + + +so until here: + I working on popup img show in page but i have problem with unmounting the page and showing some notif! + diff --git a/package.json b/package.json index 08b3c2d..16db399 100644 --- a/package.json +++ b/package.json @@ -5,39 +5,42 @@ "type": "module", "scripts": { "start": "bunx concurrently 'bun run cors' 'bun run dev'", - "dev": "vite", + "dev": "vite --host 0.0.0.0", "cors": "bun run cors-proxy.js", - "prettier" : "bunx prettier ./src --write", + "prettier": "bunx prettier ./src --write", "build": "vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { - "@tailwindcss/vite": "^4.1.7", - "axios": "^1.9.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-query": "^5.90.16", + "axios": "^1.13.2", "cors-anywhere": "^0.4.4", - "framer-motion": "^12.12.1", + "framer-motion": "^12.23.28", "path": "^0.12.7", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "tailwindcss": "^4.1.7" + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-toastify": "^11.0.5", + "tailwindcss": "^4.1.18" }, "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/node": "^24.9.1", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", - "autoprefixer": "^10.4.21", + "@eslint/js": "^9.39.2", + "@tanstack/eslint-plugin-query": "^5.91.2", + "@types/node": "^24.10.4", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.23", "concurrently": "^9.2.1", - "eslint": "^9.25.0", + "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "postcss": "^8.5.3", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^16.5.0", + "postcss": "^8.5.6", "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "typescript-eslint": "^8.51.0", + "vite": "^6.4.1" }, "trustedDependencies": [ "@tailwindcss/oxide" diff --git a/public/home.png b/public/home.png index ff66f2f..0136db3 100644 Binary files a/public/home.png and b/public/home.png differ diff --git a/src/App.tsx b/src/App.tsx index e58bf43..2eb47eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import Scoreboard from "@/components/scoreboard"; function App() { const api_cid = import.meta.env.VITE_API_CID; return ( -
+
{ return data; } -/** 🧩 Example usage in your React component or wherever */ +/** + */ +export async function fetchTeamImg(teamId: string): Promise { + const { data } = await axiosInstance.get( + `/teams/${teamId}/photo`, + { responseType: "blob" } + ) + + return data; +} + export async function loadContestData(cid: string) { const contestApi = await fetchContest(cid); const beforeApi = await fetchBefore(cid); diff --git a/src/components/scoreboard.tsx b/src/components/scoreboard.tsx index af13047..26de20c 100644 --- a/src/components/scoreboard.tsx +++ b/src/components/scoreboard.tsx @@ -1,46 +1,14 @@ import { useEffect, useState } from "react"; +import TeamImg from "./teamImg" import { AnimatePresence, motion } from "framer-motion"; -import { loadContestData } from "@/components/data.ts"; -import type { ScoreboardData, ScoreboardRow, ProblemInfo } from "../types"; +import { loadContestData } from "./data.ts"; +import type { ScoreboardData, ScoreboardRow, ProblemInfo ,ScoreboardProps } from "../types"; +import {colorBasedBg, firstSubmit} from "./utils.ts" -interface ScoreboardProps { - contestId: string | number; -} - -export function sortAndRankTeams(teams: ScoreboardRow[]): ScoreboardRow[] { - const firstSolves: Record< - string, - { time: number; team_id: string | number } - > = {}; - - // Clear first_to_solve flags and find earliest solves - teams.forEach((team) => { - team.problems.forEach((problem) => { - problem.first_to_solve = false; - if (problem.solved) { - const prev = firstSolves[problem.problem_id]; - if (!prev || problem.time < prev.time) { - firstSolves[problem.problem_id] = { - time: problem.time, - team_id: team.team_id, - }; - } - } - }); - }); - - // Mark FTS - teams.forEach((team) => { - team.problems.forEach((problem) => { - if ( - problem.solved && - firstSolves[problem.problem_id]?.team_id === team.team_id - ) { - problem.first_to_solve = true; - } - }); - }); +function sortAndRankTeams(teams: ScoreboardRow[]): ScoreboardRow[] { + firstSubmit(teams) + // Sort and assign ranks teams.sort((a, b) => { if (b.score.num_solved !== a.score.num_solved) @@ -101,6 +69,7 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { const [judges, setJudges] = useState([]); const [subs, setSubs] = useState([]); const api_penalty = import.meta.env.VITE_API_PENALTY; + const [teamPopup , setTeamPopup] = useState({state : false , id : ""}) useEffect(() => { loadContestData(contestId) @@ -189,21 +158,24 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { useEffect(() => { const handleKeyDown = (event: any) => { if (event.key === "Escape") { - console.log("Escape key pressed"); - setrCoolDown(false); // Call your function when Escape is pressed - document.querySelector(".active")?.classList.remove("active"); + if (teamPopup.state) { + setTeamPopup({state : false , id :""}) + } else { + setrCoolDown(false); // Call your function when Escape is pressed + document.querySelector(".active")?.classList.remove("active"); + } } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [setrCoolDown]); + }, [setrCoolDown,teamPopup]); const getCellColor = (pr: ProblemInfo, team_id: string) => { - if (pr.num_pending > 0) return "bg-yellow-300"; - if (pr.solved) return "bg-green-300"; - if (pr.num_judged > 0) return "bg-red-400"; + if (pr.num_pending > 0) return "bg-yellow"; + if (pr.solved) return "bg-green"; + if (pr.num_judged > 0) return "bg-red"; // Handle special case: team had a pending submission in the previous state, // but now the judged result has no penalty (e.g., compile error, too late submission, etc.). @@ -247,9 +219,6 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { const pendings = document.getElementsByClassName("cursor-pointer"); if (!pendings) return; const lastPend = pendings[pendings.length - 1]; - lastPend?.scrollIntoView({ - behavior: "smooth", - }); lastPend?.click(); } } @@ -259,14 +228,14 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { }; }, []); - return ( -
+ return (<> +

{contestData?.name}

-
+
-
+
RANK
TEAM @@ -283,7 +252,7 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { style={{ gridTemplateColumns: `repeat(${problems?.length || 4}, 1fr)`, }} - className="grid text-white gap-3 justify-center min-w-[30px]" + className="grid text-white gap-3 justify-center min-w-[30px]" > {problems?.map((problem: any, i: number) => (
- {problem.label || String.fromCharCode(65 + i)} + {problem.label || String.fromCharCode(65 + i)}
))} @@ -311,32 +280,37 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { transition={{ layout: { duration: 1.1, - ease: [0.25, 0.8, 0.25, 1], // ease-in-out + //ease: [0.25, 0.8, 0.25, 1], // ease-in-out + ease: [0.05, 0.8, 0.35, 1], }, opacity: { duration: 0.3, }, }} key={team.team_id} - initial={{ opacity: 0, y: -10 }} + initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: 10 }} + exit={{ opacity: 0, y: -5 }} style={{ zIndex: result.length - index }} - className={`team-${team.team_id} bg-white border-b-2 border-gray-100 gap-2 - [&:has(&>*.active)]:z-50 relative grid px-2 py-1 justify-center grid-cols-[50px_25rem_10rem_1fr_5rem]`} + className={`team-${team.team_id} bg-card has-[.active]:bg-card/10 card text-foreground not-last:border-b border-border/30 gap-2 + relative grid px-2 py-2 justify-center grid-cols-[50px_25rem_10rem_1fr_5rem]`} >
{team.rank}
-
- +
{ + setTeamPopup({state : true ,id : team.team_id }) + }} + className="flex flex-col justify-center cursor-pointer hover:scale-105 transition-transform"> + {getNameTeam(team.team_id)} - + {getUniTeam(team.team_id)}
-
+
{team.score.num_solved} {" "} @@ -366,7 +340,7 @@ export default function Scoreboard({ contestId }: ScoreboardProps) { }); }} id={`team-${team.team_id}-${pr.problem_id}`} - className={`relative grid py-3 place-items-center rounded-lg text-center cell font-bold text-2xl ${pr.num_pending > 0 ? "cursor-pointer" : ""} transition-all rounded ${getCellColor(pr, team.team_id)} p-1 ${pr.first_to_solve ? "bg-green-800 text-white" : ""}`} + className={`relative grid py-3 place-items-center rounded-2xl text-center cell font-bold text-2xl ${pr.num_pending > 0 ? "cursor-pointer" : ""} transition-all ${getCellColor(pr, team.team_id)} p-1 ${pr.first_to_solve ? "bg-green-800 text-white" : ""}`} > {pr.solved ? `${pr.time}` : ""} {pr.first_to_solve && ( @@ -399,5 +373,10 @@ export default function Scoreboard({ contestId }: ScoreboardProps) {
- ); + { + console.log("test") + setTeamPopup({state: false , id : ""}) + }} /> + ); + } diff --git a/src/components/teamImg.tsx b/src/components/teamImg.tsx new file mode 100644 index 0000000..3714a3c --- /dev/null +++ b/src/components/teamImg.tsx @@ -0,0 +1,34 @@ +import { fetchTeamImg } from "./data" +import { + useQuery, + useMutation, + useQueryClient, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query' + +const TeamImg = ({teamPopup , setDefault} : {teamPopup : {state : boolean ,id : string} , setDefault :() => void}) => { + if (!teamPopup.state || !teamPopup.id) return null; + + const query = useQuery({ queryKey: [teamPopup.id], queryFn: () => fetchTeamImg(teamPopup.id) }) + + if (query.error) return setDefault(); + + if (query.data){ + console.log(query.data) + const src = URL.createObjectURL(query.data) + return (<> +
+ { + query.isPending &&
Pending...!
|| + query.isError && team || + query.data && fallback + } +
+ ) + } + } + + +export default TeamImg diff --git a/src/components/utils.ts b/src/components/utils.ts new file mode 100644 index 0000000..7f813fa --- /dev/null +++ b/src/components/utils.ts @@ -0,0 +1,59 @@ +import type { ScoreboardData, ScoreboardRow, ProblemInfo } from "@/types"; + +const colorBasedBg = ( bgColor : string) => { + let r, g, b; + // ✅ Handle HEX (#fff or #ffffff) + if (bgColor.startsWith("#")) { + let hex = bgColor.replace("#", ""); + if (hex.length === 3) { + hex = hex.split("").map(c => c + c).join(""); + } + r = parseInt(hex.substring(0, 2), 16); + g = parseInt(hex.substring(2, 4), 16); + b = parseInt(hex.substring(4, 6), 16); + } + // ✅ Handle RGB / RGBA + else if (bgColor.startsWith("rgb")) { + const rgb = bgColor.match(/\d+/g).map(Number); + [r, g, b] = rgb; + } + + const luminance = (0.299 * r + 0.587 * g + 0.114 * b); + return luminance > 150 ? "#000000" : "#ffffff"; +} + +// +const firstSubmit = (teams: ScoreboardRow[]) => { + const firstSolves: Record = {}; + // Clear first_to_solve flags and find earliest solves + teams.forEach((team) => { + team.problems.forEach((problem) => { + problem.first_to_solve = false; + if (problem.solved) { + const prev = firstSolves[problem.problem_id]; + if (!prev || problem.time < prev.time) { + firstSolves[problem.problem_id] = { + time: problem.time, + team_id: team.team_id, + }; + } + } + }); + }); + + // Mark FTS + teams.forEach((team) => { + team.problems.forEach((problem) => { + if ( + problem.solved && + firstSolves[problem.problem_id]?.team_id === team.team_id + ) { + problem.first_to_solve = true; + } + }); + }); +} + + + +export {colorBasedBg, firstSubmit}; diff --git a/src/index.css b/src/index.css index 09c7a52..666881c 100644 --- a/src/index.css +++ b/src/index.css @@ -3,11 +3,12 @@ @import url("https://fonts.googleapis.com/css2?family=Luckiest+Guy&display=swap"); @import "tailwindcss"; +@import "@/tailwind/theme/charbug.css" layer(theme); +@import "@/tailwind/theme/default.css" layer(theme); body { font-family: "Vazirmatn", "Ubuntu", sans-serif; font-size: 62%; - background-color: #f9f5f0; } .title-font { @@ -32,3 +33,10 @@ body { .active { animation: blinkFocus 1.5s ease-in-out infinite; } + + +.popup { + transform : translate(-50%,-50%); + top : 50%; + left : 50%; +} diff --git a/src/main.tsx b/src/main.tsx index eff7ccc..e91caeb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,14 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; +import { QueryClientProvider , QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() createRoot(document.getElementById("root")!).render( + , + ); diff --git a/src/tailwind/theme/charbug.css b/src/tailwind/theme/charbug.css new file mode 100644 index 0000000..e31e02c --- /dev/null +++ b/src/tailwind/theme/charbug.css @@ -0,0 +1,12 @@ +@layer theme(theme-charbug) { + --background: #1e1e1e; + --foreground: #f0f0f0; + + --card: #2a2a2a; + --card-foreground: #ffffff; + + --primary: #93c5fd; + --primary-foreground: #1e1e1e; + + /* ... */ +} diff --git a/src/tailwind/theme/default.css b/src/tailwind/theme/default.css new file mode 100644 index 0000000..4b7ba23 --- /dev/null +++ b/src/tailwind/theme/default.css @@ -0,0 +1,31 @@ + +@theme { + --color-background: #e8dcc4; + --color-foreground: #1e3a5f; + + --color-card: #f5efe0; + --color-card-foreground: #1e3a5f; + + --color-yellow :#f2cf62; + --color-green : #6fae7f; + --color-green-foreground : #6fae7f; + --green-black: #0d2d24; + --color-red: #c85a54; + --color-red-foreground: #ffffff; + + + --color-primary: #1e3a5f; + ---color-primary-foreground: #ffffff; + + --color-secondary: #476e97; + --color-secondary-foreground: #ffffff; + + --accent: #c85a54; + --accent-foreground: #ffffff; + + --color-border: #1e3a5f; + --ring: #476e97; + + --destructive: #c85a54; + --destructive-foreground: #ffffff; +} diff --git a/src/types/index.ts b/src/types/index.ts index 0f323f1..c125dec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -42,3 +42,6 @@ export interface ScoreboardData { contest: ContestInfo; rows: ScoreboardRow[]; } +export interface ScoreboardProps { + contestId: string | number; +}