├── .eslintrc.json
├── .vscode
└── settings.json
├── assets
├── Blanka.otf
└── jwoc_logo.svg
├── public
├── favicon.ico
└── vercel.svg
├── postcss.config.js
├── atoms
├── modalAtom.ts
└── selectedUserAtom.ts
├── next-env.d.ts
├── pages
├── _app.tsx
├── api
│ └── hello.ts
└── index.tsx
├── backend
├── utils
│ ├── writeJson.js
│ └── allRepos.js
└── generate.js
├── types
└── index.ts
├── utils
├── parseName.ts
└── generateConfetti.ts
├── next.config.js
├── .gitignore
├── tsconfig.json
├── components
├── Table.tsx
├── Footer.tsx
├── MetaTags.tsx
├── Header.tsx
├── TableHeader.tsx
├── EachPR.tsx
├── PRModal.tsx
├── TopCard.tsx
└── UserCard.tsx
├── .github
└── workflows
│ └── main.yml
├── package.json
├── tailwind.config.js
├── README.md
└── styles
└── globals.css
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib"
3 | }
--------------------------------------------------------------------------------
/assets/Blanka.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niloysikdar/jwoc-leaderboard/HEAD/assets/Blanka.otf
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niloysikdar/jwoc-leaderboard/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/atoms/modalAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 |
3 | export const modalState = atom({
4 | key: "modal",
5 | default: false,
6 | });
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { RecoilRoot } from "recoil";
4 |
5 | function MyApp({ Component, pageProps }: AppProps) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default MyApp;
14 |
--------------------------------------------------------------------------------
/backend/utils/writeJson.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | const writeJson = (data) => {
4 | fs.writeFile("data.json", JSON.stringify(data), (error) => {
5 | if (error) {
6 | console.log(error);
7 | } else {
8 | console.log("File has been written successfully");
9 | }
10 | });
11 | };
12 |
13 | module.exports = { writeJson };
14 |
--------------------------------------------------------------------------------
/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface TableDataType {
2 | user_name: string;
3 | avatar_url: string;
4 | user_url: string;
5 | total_points: number;
6 | pr_urls: PRDataType[];
7 | full_name: string;
8 | college: string;
9 | rank: number;
10 | }
11 |
12 | interface PRDataType {
13 | url: string;
14 | difficulty: string;
15 | phase: number;
16 | }
17 |
--------------------------------------------------------------------------------
/atoms/selectedUserAtom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 |
3 | export const selectedUserState = atom({
4 | key: "pr",
5 | default: {
6 | user_name: "",
7 | avatar_url: "",
8 | user_url: "",
9 | total_points: 0,
10 | pr_urls: [
11 | {
12 | url: "",
13 | difficulty: "",
14 | phase: 0,
15 | },
16 | ],
17 | full_name: "",
18 | college: "",
19 | rank: 0,
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/utils/parseName.ts:
--------------------------------------------------------------------------------
1 | export const parseName = (name: string): string => {
2 | if (name.length === 0) {
3 | return "";
4 | }
5 | if (name.split(" ").length !== 2) {
6 | return name;
7 | } else {
8 | const [firstName, lastName] = name.split(" ");
9 | const newFirst =
10 | firstName.charAt(0).toUpperCase() + firstName.toLowerCase().substring(1);
11 | const newLast =
12 | lastName.charAt(0).toUpperCase() + lastName.toLowerCase().substring(1);
13 |
14 | return `${newFirst} ${newLast}`;
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const intercept = require("intercept-stdout");
3 |
4 | // safely ignore recoil stdout warning messages
5 | function interceptStdout(text) {
6 | if (text.includes("Duplicate atom key")) {
7 | return "";
8 | }
9 | return text;
10 | }
11 |
12 | // Intercept in dev and prod
13 | intercept(interceptStdout);
14 |
15 | const nextConfig = {
16 | reactStrictMode: true,
17 | images: {
18 | domains: ["avatars.githubusercontent.com"],
19 | },
20 | };
21 |
22 | module.exports = nextConfig;
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/components/Table.tsx:
--------------------------------------------------------------------------------
1 | import { TableDataType } from "../types";
2 | import TableHeader from "./TableHeader";
3 | import UserCard from "./UserCard";
4 |
5 | const Table = ({ data }: { data: TableDataType[] }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | {data.map((item, i) => (
14 |
15 | ))}
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default Table;
24 |
--------------------------------------------------------------------------------
/utils/generateConfetti.ts:
--------------------------------------------------------------------------------
1 | import confetti from "canvas-confetti";
2 |
3 | var end = Date.now() + 5 * 1000;
4 |
5 | var colors = ["#ff1700", "#5800ff"];
6 |
7 | export const generateConfetti = () => {
8 | (function frame() {
9 | confetti({
10 | particleCount: 2,
11 | angle: 60,
12 | spread: 100,
13 | origin: { x: 0 },
14 | colors: colors,
15 | shapes: ["circle", "square"],
16 | });
17 | confetti({
18 | particleCount: 2,
19 | angle: 120,
20 | spread: 100,
21 | origin: { x: 1 },
22 | colors: colors,
23 | shapes: ["circle", "square"],
24 | });
25 |
26 | if (Date.now() < end) {
27 | requestAnimationFrame(frame);
28 | }
29 | })();
30 | };
31 |
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Logo from "../assets/jwoc_logo.svg";
3 |
4 | const Footer = () => {
5 | return (
6 |
18 | );
19 | };
20 |
21 | export default Footer;
22 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Cron Job
2 |
3 | on:
4 | schedule:
5 | # At every 3 hours
6 | - cron: "0 */3 * * *"
7 |
8 | jobs:
9 | fetch:
10 | name: FetchData
11 | runs-on: windows-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: npm install, check, build, and test using Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - run: npm install
20 | - run: npm run generate
21 | env:
22 | GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
23 | MONGODB_URL: ${{ secrets.MONGODB_URL }}
24 |
25 | - uses: stefanzweifel/git-auto-commit-action@v4
26 | with:
27 | commit_message: "feat: updated data using GitHub workflow"
28 | commit_user_name: niloysikdar
29 | commit_user_email: niloysikdar30@gmail.com
30 | commit_author: Niloy Sikdar
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jwoc-leaderboard",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint",
9 | "generate": "node backend/generate.js"
10 | },
11 | "dependencies": {
12 | "@heroicons/react": "^1.0.5",
13 | "axios": "^0.25.0",
14 | "canvas-confetti": "^1.5.1",
15 | "dotenv": "^16.0.0",
16 | "fuse.js": "^6.5.3",
17 | "intercept-stdout": "^0.1.2",
18 | "mongodb": "^4.3.1",
19 | "next": "12.0.10",
20 | "react": "17.0.2",
21 | "react-dom": "17.0.2",
22 | "recoil": "^0.6.1",
23 | "sharp": "^0.30.1"
24 | },
25 | "devDependencies": {
26 | "@types/canvas-confetti": "^1.4.2",
27 | "@types/node": "17.0.17",
28 | "@types/react": "17.0.39",
29 | "autoprefixer": "^10.4.2",
30 | "eslint": "8.8.0",
31 | "eslint-config-next": "12.0.10",
32 | "postcss": "^8.4.6",
33 | "tailwindcss": "^3.0.19",
34 | "typescript": "4.5.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | "./pages/**/*.{js,ts,jsx,tsx}",
4 | "./components/**/*.{js,ts,jsx,tsx}",
5 | ],
6 | theme: {
7 | screens: {
8 | "2xl": { max: "1535px" },
9 | xl: { max: "1279px" },
10 | lg: { max: "1023px" },
11 | md: { max: "767px" },
12 | sm: { max: "639px" },
13 | xs: { max: "325px" },
14 | },
15 | extend: {
16 | colors: {
17 | darkblack: "#222831",
18 | lightblack: "#3c434e",
19 | darkwhite: "#ffffff",
20 | lightwhite: "#f7fafc",
21 | darkgrey: "#787a91",
22 | lightgrey: "#9ba4b4",
23 | primaryoff: "#99feff",
24 | primarylight: "#385cf0",
25 | primarydark: "#1d4cb0",
26 | highlight: "#f7f0c2",
27 | warningoff: "#ff3838",
28 | discordBg: "#404eed",
29 | },
30 |
31 | fontFamily: {
32 | mainfont: ["Inter", "sans-serif"],
33 | codefont: ["Outfit", "sans-serif"],
34 | curlfont: ["Quicksand", "sans-serif"],
35 | },
36 | },
37 | },
38 | extend: {},
39 | plugins: [],
40 | };
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
JWOC Leaderboard
5 |
6 |
7 | Official leaderboard for JWOC 2k22
8 |
9 |
10 | Visit Here
11 | ·
12 | YouTube Live
13 | ·
14 | Report Bug
15 |
16 |
17 |
18 | ### Star the [GitHub repo](https://github.com/niloysikdar/jwoc-leaderboard) to keep the developer motivated ✨
19 |
20 | ### Built With
21 |
22 | - [React](https://reactjs.org)
23 | - [Next.js](https://nextjs.org)
24 | - [TypeScript](https://www.typescriptlang.org)
25 | - [TailwindCSS](https://tailwindcss.com)
26 | - [Recoil](https://recoiljs.org)
27 | - [MongoDB](https://www.mongodb.com)
28 | - [GitHub API](https://docs.github.com/en/rest)
29 | - [GitHub Actions](https://docs.github.com/en/actions)
30 |
--------------------------------------------------------------------------------
/components/MetaTags.tsx:
--------------------------------------------------------------------------------
1 | const MetaTags = () => {
2 | return (
3 | <>
4 |
5 |
6 |
10 |
11 |
12 |
16 |
20 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
34 |
35 | >
36 | );
37 | };
38 |
39 | export default MetaTags;
40 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import { HomeIcon } from "@heroicons/react/solid";
4 | import Logo from "../assets/jwoc_logo.svg";
5 |
6 | const Header = () => {
7 | return (
8 |
31 | );
32 | };
33 |
34 | export default Header;
35 |
--------------------------------------------------------------------------------
/components/TableHeader.tsx:
--------------------------------------------------------------------------------
1 | const TableHeader = () => {
2 | return (
3 |
4 |
5 | |
6 | SL No.
7 | |
8 |
9 | Rank
10 | |
11 |
12 | Participant's Info
13 | |
14 |
15 | GitHub Handle
16 | |
17 |
18 | View All PRs
19 | |
20 |
21 | Points
22 | |
23 |
24 |
25 | );
26 | };
27 |
28 | export default TableHeader;
29 |
--------------------------------------------------------------------------------
/backend/utils/allRepos.js:
--------------------------------------------------------------------------------
1 | const allRepos = [
2 | "chiraag-kakar/createnshare",
3 | "niloysikdar/Get-Set-Go",
4 | "niloysikdar/Kahani",
5 | "niloysikdar/CFI-JGEC",
6 | "maityamit/Tracky-Track-your-goals-or-targets",
7 | "Kajal13081/Musgenix",
8 | "Kajal13081/Gallery-Application",
9 | "harshita2216/hello-jobs",
10 | "Ly0kami/QuotesApp-JWoC",
11 | "theblockchainchief/web3-hub",
12 | "BRAVO68WEB/url-minify",
13 | "HITK-TECH-Community/Community-Website",
14 | "The-Shivam-garg/BigB-E-learn-Websit-e",
15 | "Codehackerone/Medify",
16 | "aman34503/Go-Airbnb",
17 | "SauravMukherjee44/Aec-Library-Website",
18 | "riyajha2305/Instagram-Clone",
19 | "SaraswatGit/PlanZap",
20 | "abhisheks008/ML-Crate",
21 | "opticSquid/hive",
22 | "gdscjgec/Image-Editor",
23 | "prathimacode-hub/Treksy",
24 | "gdscjgec/Pictionary",
25 | "prathimacode-hub/MedFlare",
26 | "ZiaCodes/counter.js",
27 | "ZiaCodes/claim-prize",
28 | "shibam-naskar/wally",
29 | "harshita214/Chrome-Extension",
30 | "imniladri/DevHub",
31 | "skpandey885/HealhUb",
32 | "sahilsaha7773/SpotLight",
33 | "shibam-naskar/IMAGE_COMPRESOR",
34 | "mohit200008/medi-Care",
35 | "HackClubRAIT/HackClubRAIT-Website.github.io",
36 | "Saup21/Code-Sandbox",
37 | "agamjotsingh18/pollitup",
38 | "neelshah2409/Bot-Collection",
39 | "BlueBlaze6335/Illicit-Illustrations",
40 | "codejay411/Fake-News-Detection-App",
41 | "sagnik1511/Tabular-AutoML",
42 | "Tech-N-Science/FunwithScience",
43 | "smaranjitghose/awesome-portfolio-websites",
44 | "IAmTamal/Milan",
45 | "smaranjitghose/indokhaadyam",
46 | "7saikat7/supply_chain",
47 | ];
48 |
49 | module.exports = { allRepos };
50 |
--------------------------------------------------------------------------------
/components/EachPR.tsx:
--------------------------------------------------------------------------------
1 | const EachPR = ({
2 | link,
3 | difficulty,
4 | phase,
5 | }: {
6 | link: string;
7 | difficulty: string;
8 | phase: number;
9 | }) => {
10 | const { bgColor, textColor } = getColor(difficulty);
11 |
12 | return (
13 | <>
14 |
15 |
21 | {link}
22 |
23 |
24 |
25 | Difficulty:
26 |
29 | {difficulty}
30 |
31 |
32 |
33 | Phase: {phase}
34 |
35 |
36 |
37 | >
38 | );
39 | };
40 |
41 | const getColor = (difficulty: string) => {
42 | let result = { bgColor: "", textColor: "" };
43 |
44 | const bgValues = {
45 | easy: "bg-green-200",
46 | medium: "bg-yellow-200",
47 | hard: "bg-red-200",
48 | };
49 | const textColor = {
50 | easy: "text-green-700",
51 | medium: "text-yellow-700",
52 | hard: "text-red-700",
53 | };
54 |
55 | if (difficulty === "Easy") {
56 | result.bgColor = bgValues.easy;
57 | result.textColor = textColor.easy;
58 | } else if (difficulty === "Medium") {
59 | result.bgColor = bgValues.medium;
60 | result.textColor = textColor.medium;
61 | } else if (difficulty === "Hard") {
62 | result.bgColor = bgValues.hard;
63 | result.textColor = textColor.hard;
64 | }
65 |
66 | return result;
67 | };
68 |
69 | export default EachPR;
70 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | /* **************************** Fonts **************************** */
2 |
3 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
4 | @import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap");
5 | @import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@500;600;700&display=swap");
6 | @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@500;600;700;800&display=swap");
7 |
8 | /* **************************** Site General Settings **************************** */
9 |
10 | * {
11 | margin: 0;
12 | padding: 0;
13 | }
14 |
15 | @tailwind base;
16 | @tailwind components;
17 | @tailwind utilities;
18 |
19 | /* **************************** Body Settings **************************** */
20 |
21 | body {
22 | background: #ffffff;
23 | font-family: "Inter", sans-serif;
24 | }
25 |
26 | /* **************************** Scroll Bar **************************** */
27 |
28 | ::-webkit-scrollbar {
29 | width: 8px;
30 | height: 8px;
31 | background: transparent;
32 | }
33 | ::-webkit-scrollbar-thumb {
34 | background: #222831;
35 | border-radius: 2px;
36 | }
37 |
38 | #div_ScrollBar::-webkit-scrollbar {
39 | width: 6px;
40 | height: 6px;
41 | background: transparent;
42 | }
43 | #div_ScrollBar::-webkit-scrollbar-track {
44 | background: #e2e8f0;
45 | border-radius: 4px;
46 | }
47 | #div_ScrollBar::-webkit-scrollbar-thumb {
48 | background: #94a3b8;
49 | border-radius: 4px;
50 | }
51 |
52 | /* **************************** Selection **************************** */
53 |
54 | ::selection {
55 | color: #222831;
56 | background: #f7f0c2;
57 | }
58 |
59 | /* **************************** User Select None **************************** */
60 |
61 | img,
62 | .noselect {
63 | user-select: none;
64 | -webkit-user-select: none;
65 | -moz-user-select: none;
66 | -ms-user-select: none;
67 |
68 | pointer-events: none;
69 |
70 | -webkit-user-drag: none;
71 | -moz-user-drag: none;
72 | -o-user-drag: none;
73 | }
74 |
75 | .user_image {
76 | position: relative !important;
77 | }
78 |
79 | /* **************************** Custom Fonts **************************** */
80 |
81 | @font-face {
82 | font-family: "Blanka";
83 | src: url(../assets/Blanka.otf);
84 | }
85 |
86 | /* **************************** Styles End **************************** */
87 |
--------------------------------------------------------------------------------
/components/PRModal.tsx:
--------------------------------------------------------------------------------
1 | import EachPR from "./EachPR";
2 | import { XIcon } from "@heroicons/react/outline";
3 | import { useRecoilState, useRecoilValue } from "recoil";
4 | import { modalState } from "../atoms/modalAtom";
5 | import { selectedUserState } from "../atoms/selectedUserAtom";
6 | import { TableDataType } from "../types";
7 | import { parseName } from "../utils/parseName";
8 |
9 | const PRModal = () => {
10 | const [val, setModalOpen] = useRecoilState(modalState);
11 | const userData: TableDataType = useRecoilValue(selectedUserState);
12 |
13 | return (
14 |
15 |
16 |
17 | setModalOpen(false)}
20 | />
21 |
22 |
23 |
24 | Name:
25 |
26 | {parseName(userData.full_name)}
27 |
28 |
29 |
30 | Total Points:
31 |
32 | {userData.total_points}
33 |
34 |
35 |
36 | Total PR Count:
37 |
38 | {userData.pr_urls.length}
39 |
40 |
41 |
42 |
43 | PR Links:
44 |
45 |
46 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default PRModal;
68 |
--------------------------------------------------------------------------------
/components/TopCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { useRecoilState } from "recoil";
3 | import { modalState } from "../atoms/modalAtom";
4 | import { selectedUserState } from "../atoms/selectedUserAtom";
5 | import { TableDataType } from "../types";
6 | import { parseName } from "../utils/parseName";
7 |
8 | const TopCard = ({ userData }: { userData: TableDataType }) => {
9 | const [i, setModal] = useRecoilState(modalState);
10 | const [j, setUserData] = useRecoilState(selectedUserState);
11 |
12 | return (
13 |
14 |
15 |
16 | {userData.total_points}
17 |
18 |
19 | Points
20 |
21 |
22 | Rank: {userData.rank}
23 |
24 |
25 |
26 |
27 |
28 |
36 |
37 |
38 | {parseName(userData.full_name)}
39 |
40 |
46 | {userData.user_name}
47 |
48 |
49 | {userData.college}
50 |
51 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default TopCard;
66 |
--------------------------------------------------------------------------------
/components/UserCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { useRecoilState } from "recoil";
3 | import { modalState } from "../atoms/modalAtom";
4 | import { selectedUserState } from "../atoms/selectedUserAtom";
5 | import { TableDataType } from "../types";
6 | import { parseName } from "../utils/parseName";
7 |
8 | const UserCard = ({ data, index }: { data: TableDataType; index: number }) => {
9 | const [isModalOpen, setModalOpen] = useRecoilState(modalState);
10 | const [userData, setUserData] = useRecoilState(selectedUserState);
11 |
12 | return (
13 |
14 | |
15 |
16 | {index + 4}
17 |
18 | |
19 |
20 |
21 | # {data.rank}
22 |
23 | |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 | {parseName(data.full_name) || (
38 | Name not found...
39 | )}
40 |
41 |
42 | {data.college || (
43 | College not found...
44 | )}
45 |
46 |
47 |
48 | |
49 |
50 |
56 | {data.user_name}
57 |
58 | |
59 |
60 | {
63 | setModalOpen(true);
64 | setUserData(data);
65 | }}
66 | >
67 | View All PRs
68 |
69 | |
70 |
71 |
72 | {data.total_points}
73 |
74 | |
75 |
76 | );
77 | };
78 |
79 | export default UserCard;
80 |
--------------------------------------------------------------------------------
/assets/jwoc_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from "next";
2 | import Head from "next/head";
3 | import Script from "next/script";
4 | import { ChangeEvent, useState, useEffect } from "react";
5 | import Fuse from "fuse.js";
6 | import { useRecoilValue } from "recoil";
7 | import { modalState } from "../atoms/modalAtom";
8 |
9 | import Table from "../components/Table";
10 | import Header from "../components/Header";
11 | import Footer from "../components/Footer";
12 | import MetaTags from "../components/MetaTags";
13 |
14 | import data from "../data.json";
15 | import PRModal from "../components/PRModal";
16 | import TopCard from "../components/TopCard";
17 | import { TableDataType } from "../types";
18 | import { generateConfetti } from "../utils/generateConfetti";
19 |
20 | const Home: NextPage = () => {
21 | const isModalOpen = useRecoilValue(modalState);
22 |
23 | const [tableData, setTableData] = useState(
24 | data.data.slice(3, data.data.length)
25 | );
26 | const [searchText, setSearchText] = useState("");
27 | const [searchedData, setSearchedData] = useState();
28 |
29 | const handleSearch = (
30 | e: ChangeEvent
31 | ) => {
32 | setSearchText(e.target.value);
33 | const fuse = new Fuse(tableData as TableDataType[], {
34 | keys: ["user_name", "full_name", "college"],
35 | threshold: 0.2,
36 | });
37 | const result = fuse.search(e.target.value).map((item) => item.item);
38 | setSearchedData(result);
39 | };
40 |
41 | useEffect(() => {
42 | generateConfetti();
43 | }, []);
44 |
45 | return (
46 | <>
47 |
48 | Leaderboard | JWoC 2K22
49 |
54 |
55 |
56 |
57 |
61 |
62 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | JWoC 2K22 Leaderboard
78 |
79 |
80 | Check your rank here!
81 |
82 |
83 | Last updated on:
84 |
85 | {new Date(data.lastUpdated).toLocaleString("en-US", {
86 | dateStyle: "full",
87 | timeStyle: "full",
88 | })}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
107 |
108 |
109 |
112 |
113 |
114 |
115 | {isModalOpen && }
116 |
117 |
118 | >
119 | );
120 | };
121 |
122 | export default Home;
123 |
--------------------------------------------------------------------------------
/backend/generate.js:
--------------------------------------------------------------------------------
1 | const { default: axios } = require("axios");
2 | const { MongoClient } = require("mongodb");
3 | const { writeJson } = require("./utils/writeJson");
4 |
5 | const dotenv = require("dotenv");
6 | dotenv.config();
7 | const ACCESS_TOKEN = process.env.GH_ACCESS_TOKEN;
8 |
9 | const { allRepos } = require("./utils/allRepos");
10 | const repos = allRepos;
11 |
12 | const eventLabel = "jwoc";
13 | const levelsData = {
14 | easy: "Easy",
15 | medium: "Medium",
16 | hard: "Hard",
17 | };
18 | let finalData = [];
19 |
20 | const MONGODB_URL = process.env.MONGODB_URL;
21 | const client = new MongoClient(MONGODB_URL);
22 |
23 | const fetchAllData = async () => {
24 | for (let i = 0; i < repos.length; i++) {
25 | const repoName = repos[i];
26 | const data = await fetchRepoData(repoName);
27 | console.log(`Completed for: ${repos[i]}`);
28 | console.log("!.!.!.!.!.!.!");
29 | console.log("*************");
30 | finalData = [...finalData, ...data];
31 | }
32 |
33 | let leaderboardData = generateRank(finalData).sort((a, b) =>
34 | a.total_points < b.total_points ? 1 : -1
35 | );
36 |
37 | let rank = 1;
38 |
39 | for (let pos = 0; pos < leaderboardData.length; pos++) {
40 | const currentData = leaderboardData[pos];
41 |
42 | const { full_name, college } = await getDatafromDB(currentData.user_name);
43 |
44 | currentData.full_name = full_name;
45 | currentData.college = college;
46 |
47 | if (pos === 0) {
48 | currentData.rank = rank;
49 | } else {
50 | const prevData = leaderboardData[pos - 1];
51 | if (prevData.total_points > currentData.total_points) {
52 | rank++;
53 | currentData.rank = rank;
54 | } else {
55 | currentData.rank = rank;
56 | }
57 | }
58 | }
59 |
60 | writeJson({
61 | lastUpdated: new Date(),
62 | data: leaderboardData,
63 | });
64 | };
65 |
66 | const getDatafromDB = async (userName) => {
67 | let finalData = { full_name: "", college: "" };
68 | try {
69 | const db = client.db("jwoc");
70 | const collection = db.collection("mentees");
71 | const data = await collection.findOne({ $text: { $search: userName } });
72 | if (data) {
73 | finalData.full_name = data.name;
74 | finalData.college = data.college;
75 | }
76 | } catch (error) {
77 | console.log(error);
78 | }
79 |
80 | return finalData;
81 | };
82 |
83 | let counter = 0;
84 |
85 | const fetchRepoData = async (repoName) => {
86 | let pageCount = 1;
87 | let pageAvailabe = true;
88 | let allData = [];
89 |
90 | while (pageAvailabe) {
91 | const reqUrl = `https://api.github.com/repos/${repoName}/pulls?state=closed&per_page=100&page=${pageCount}`;
92 | try {
93 | counter++;
94 | console.log(
95 | `${counter}. Fetching data for: ${repoName} and pageCount: ${pageCount}`
96 | );
97 | const res = await axios.get(reqUrl, {
98 | headers: {
99 | authorization: `token ${ACCESS_TOKEN}`,
100 | "User-Agent": "request",
101 | Accept: "application/vnd.github.v3+json",
102 | },
103 | });
104 | const resData = res.data;
105 |
106 | console.log(
107 | `Data has been fetched for: ${repoName} and pageCount: ${pageCount}`
108 | );
109 | // await sleep(2000);
110 | console.log("_____________");
111 |
112 | if (resData.length !== 0) {
113 | const jwocData = filterJwoc(resData);
114 | allData = [...allData, ...jwocData];
115 | pageCount++;
116 | } else {
117 | pageAvailabe = false;
118 | }
119 | } catch (error) {
120 | console.log(error.message);
121 | pageAvailabe = false;
122 | process.exit(1);
123 | }
124 | }
125 |
126 | return allData;
127 | };
128 |
129 | const filterJwoc = (allData) => {
130 | let finalData = [];
131 |
132 | const jwocData = allData.filter((prData) => {
133 | let isJwoc = false;
134 | if (prData.merged_at) {
135 | prData.labels.map((eachLabel) => {
136 | if (eachLabel.name.toLowerCase().includes(eventLabel.toLowerCase())) {
137 | isJwoc = true;
138 | }
139 | });
140 | }
141 |
142 | return isJwoc;
143 | });
144 |
145 | if (jwocData.length !== 0) {
146 | jwocData.map((prData) => {
147 | const data = {
148 | user_name: prData.user.login,
149 | avatar_url: prData.user.avatar_url,
150 | user_url: prData.user.html_url,
151 | pr_url: prData.html_url,
152 | labels: prData.labels.map((labelData) => labelData.name),
153 | phase: getPhase(prData.created_at),
154 | };
155 | finalData = [...finalData, data];
156 | });
157 | }
158 |
159 | return finalData;
160 | };
161 |
162 | const getPhase = (created_at) => {
163 | const phase1deadlineISO = "2022-02-28T18:31:00.000Z";
164 | const deadline1 = new Date(phase1deadlineISO);
165 | const createdAtDate = new Date(created_at);
166 |
167 | if (createdAtDate > deadline1) return 2;
168 | return 1;
169 | };
170 |
171 | const generateRank = (fullData) => {
172 | let finalData = [];
173 |
174 | fullData.map((eachPrData) => {
175 | const index = finalData.findIndex(
176 | (data) => data.user_name === eachPrData.user_name
177 | );
178 |
179 | const { point, difficulty } = getPoints(
180 | eachPrData.labels,
181 | eachPrData.phase
182 | );
183 |
184 | if (index === -1) {
185 | const userData = {
186 | user_name: eachPrData.user_name,
187 | avatar_url: eachPrData.avatar_url,
188 | user_url: eachPrData.user_url,
189 | total_points: point,
190 | pr_urls: [
191 | {
192 | url: eachPrData.pr_url,
193 | difficulty: difficulty,
194 | phase: eachPrData.phase,
195 | },
196 | ],
197 | };
198 |
199 | finalData = [...finalData, userData];
200 | } else {
201 | finalData[index].total_points += point;
202 | finalData[index].pr_urls.push({
203 | url: eachPrData.pr_url,
204 | difficulty: difficulty,
205 | phase: eachPrData.phase,
206 | });
207 | }
208 | });
209 |
210 | return finalData;
211 | };
212 |
213 | const getPoints = (labelsArray, phase) => {
214 | let point = 0,
215 | difficulty = "";
216 |
217 | labelsArray.map((label) => {
218 | if (label.toLowerCase().includes(levelsData.easy.toLowerCase())) {
219 | difficulty = levelsData.easy;
220 | if (phase === 1) {
221 | point = 1;
222 | } else {
223 | point = 2;
224 | }
225 | }
226 | if (label.toLowerCase().includes(levelsData.medium.toLowerCase())) {
227 | difficulty = levelsData.medium;
228 | if (phase === 1) {
229 | point = 3;
230 | } else {
231 | point = 4;
232 | }
233 | }
234 | if (label.toLowerCase().includes(levelsData.hard.toLowerCase())) {
235 | difficulty = levelsData.hard;
236 | if (phase === 1) {
237 | point = 5;
238 | } else {
239 | point = 8;
240 | }
241 | }
242 | });
243 |
244 | return { point, difficulty };
245 | };
246 |
247 | client
248 | .connect()
249 | .then(() => fetchAllData().then(() => client.close()))
250 | .catch((error) => console.log(error.message));
251 |
--------------------------------------------------------------------------------