diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7546d5a..f94e6d3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: latest @@ -45,7 +45,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: latest @@ -66,7 +66,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: latest @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: latest diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2e7f2e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,110 @@ +## [1.0.0-development.7](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.6...1.0.0-development.7) (2024-1-16) + + +### :sparkles: Features + +* **api:search:** add endpoint `/api/search/[searchStr]` ([bf15e83](https://github.com/Clumsy-Coder/uva-uhunt/commit/bf15e83c1e2b6ef378b728c2d62ed5f7f2ed6d31)) +* **components:search:** add `searchbar` component ([d8acdd9](https://github.com/Clumsy-Coder/uva-uhunt/commit/d8acdd9a980a5a978993dbbf58430f0aa59fb138)) + + +### :bug: Bug Fixes + +* **api:users:submissions:overtime:** submission with one year ([8b69ffa](https://github.com/Clumsy-Coder/uva-uhunt/commit/8b69ffa982d53fce71804ccd10d2aada9a8acfa4)) + +## [1.0.0-development.6](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.5...1.0.0-development.6) (2024-1-15) + + +### :sparkles: Features + +* **api:user:submissions:verdict:** add endpoint `/api/users/[username]/submissions/verdict` ([33101e6](https://github.com/Clumsy-Coder/uva-uhunt/commit/33101e61c3c9e9b44bb9a0f76bc6b30f2fd670f2)) +* **api:user:submissions:** add endpoint `/api/users/[username]/submissions` ([023d886](https://github.com/Clumsy-Coder/uva-uhunt/commit/023d8865d3603325c11185d2ee7e4896f4a7c211)) +* **api:users:attempted:** add endpoint `/api/users/[username]/submissions/attempted` ([f3ea62c](https://github.com/Clumsy-Coder/uva-uhunt/commit/f3ea62c89d2804b5c8dcdcd2a602434d1bcccd35)) +* **api:users:submissions:** add endpoint `/api/users/[username]/submissions/language` ([d773671](https://github.com/Clumsy-Coder/uva-uhunt/commit/d773671ab833be365a0e368c4ad3ffc43b030084)) +* **api:users:submissions:** add endpoint `/api/users/[username]/submissions/overtime` ([01232a7](https://github.com/Clumsy-Coder/uva-uhunt/commit/01232a7ca78c1f9a648a3ada5b74b9664b2d1642)) +* **components:charts:** add `SolvedVsAttemptedDonutChart` component ([25bda0e](https://github.com/Clumsy-Coder/uva-uhunt/commit/25bda0ebbd5cb59b31f4c258d6dd31f5f6263fc4)) +* **page:user:** display problem solved VS user submissions with donut chart ([e2689d5](https://github.com/Clumsy-Coder/uva-uhunt/commit/e2689d5f8ab121f86a7f894ae2dd5717d074b769)) +* **page:user:** display user submissions by language with radar chart ([54639ab](https://github.com/Clumsy-Coder/uva-uhunt/commit/54639abedcb68f405f6cd314547b6cffe9586934)) +* **page:user:** display user submissions by verdict with bar chart ([2d8a058](https://github.com/Clumsy-Coder/uva-uhunt/commit/2d8a058e5e1af1dc0c3bdb97b0d0bed74408cd47)) +* **page:user:** display user submissions on `/users/[username]` page ([c937b78](https://github.com/Clumsy-Coder/uva-uhunt/commit/c937b7871af9c72c784dcf80197252eaf72ca318)) +* **page:user:** display user submissions overtime with area chart ([21ea5f1](https://github.com/Clumsy-Coder/uva-uhunt/commit/21ea5f150e7c62bb82844b550350454fc812c130)) + +## [1.0.0-development.5](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.4...1.0.0-development.5) (2024-1-12) + + +### :sparkles: Features + +* **api:problemNum:ranklist:** add endpoint `/api/problems/ranklist/[problemNum]` ([1a35cec](https://github.com/Clumsy-Coder/uva-uhunt/commit/1a35cec38173c12ce1078dda8795cc807ebacbd3)) +* **api:problemNum:submission:** add endpoint `/api/submissions/[problemNum]` ([36ed144](https://github.com/Clumsy-Coder/uva-uhunt/commit/36ed1442d43c849501ce540771ec65c300b968ba)) +* **api:submission:language:** add endpoint `/api/submissions/language/[problemNum]` ([fd4c856](https://github.com/Clumsy-Coder/uva-uhunt/commit/fd4c856a0f7943d1476afdc714a726daf3747bbd)) +* **api:submission:** add endpoint `/api/submissions/overtime/[problemNum]` ([304a563](https://github.com/Clumsy-Coder/uva-uhunt/commit/304a5632393094c51671213156267d1d73968ba5)) +* **components:charts:** add `ProblemVerdictChart` component ([2104011](https://github.com/Clumsy-Coder/uva-uhunt/commit/2104011cc6811b4d854ef7e2c88e52cd373046e8)) +* **components:charts:** add `SubmissionLanguageRadarChart` component ([fc5ecfc](https://github.com/Clumsy-Coder/uva-uhunt/commit/fc5ecfc46270ddee9da516ca2952c51232d65a33)) +* **components:charts:** add `SubmissionsOvertimeChart` component ([16d1311](https://github.com/Clumsy-Coder/uva-uhunt/commit/16d1311f4862b8c751cf69c1eeeb2ce5ef8b927a)) +* **components:charts:** add Recharts custom tooltip component ([b6ef657](https://github.com/Clumsy-Coder/uva-uhunt/commit/b6ef6576adaa4d3c2c41244e8abd637f44da09b0)) +* **components:** add `VirtualTable` component ([09259ea](https://github.com/Clumsy-Coder/uva-uhunt/commit/09259ea6136050bb7a0338efd6afdabc4225b18b)) +* **page:problemNum:** fetch stats for `/problems/[problemNum]` page ([afe0752](https://github.com/Clumsy-Coder/uva-uhunt/commit/afe075219fcb5d52a165228ef2d7210c91bf8aa9)) +* **page:problemNum:** render `ProblemVerdictChart` component ([acc2eac](https://github.com/Clumsy-Coder/uva-uhunt/commit/acc2eac151fb0b8c5713df1007c8ad627a0b7fac)) +* **page:problemNum:** render `SubmissionLanguageRadarChart` component ([6ebcb52](https://github.com/Clumsy-Coder/uva-uhunt/commit/6ebcb5272a10374aca6cb3a6bb5a847c3b4ee890)) +* **page:problemNum:** render `SubmissionOvertimeChart` component ([3f2f7c4](https://github.com/Clumsy-Coder/uva-uhunt/commit/3f2f7c4e8af0eeb57736696c36d2164f8572013a)) +* **page:problemNum:** render `VirtualTable` for `Problem submissions` ([d271d90](https://github.com/Clumsy-Coder/uva-uhunt/commit/d271d90ee05dfd43f308c453e9d42947f52d3392)) +* **page:problemNum:** render `VirtualTable` for ranklist ([e3ac245](https://github.com/Clumsy-Coder/uva-uhunt/commit/e3ac24512fd3c74f2f3865cfd1e841e19d2bedd3)) +* **page:problemNum:** set title as link to view problem pdf ([ffd247c](https://github.com/Clumsy-Coder/uva-uhunt/commit/ffd247c5f908514de35828902d65e7840d418124)) +* **shadcn:data-table:** set DataTable height if provided ([c58b14f](https://github.com/Clumsy-Coder/uva-uhunt/commit/c58b14f0c83aaa4a32717dc10716ff25c1a243e8)) + +## [1.0.0-development.4](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.3...1.0.0-development.4) (2024-1-3) + + +### :sparkles: Features + +* **components:data-table:** display `language` in live submissions data-table ([dd6e3db](https://github.com/Clumsy-Coder/uva-uhunt/commit/dd6e3dbfc0e0d34d861bcb258fea359dcace996c)) +* **components:data-table:** display `problem number` in live submissions data-table ([44f75d5](https://github.com/Clumsy-Coder/uva-uhunt/commit/44f75d5e7636f3155f022f4a11d0677df5c41cc1)) +* **components:data-table:** display `problem title` in live submissions data-table ([6c131eb](https://github.com/Clumsy-Coder/uva-uhunt/commit/6c131eb60bd4c94e8cd5d68d10d71cca1d3c1527)) +* **components:data-table:** display `rank` in live submissions data-table ([293387b](https://github.com/Clumsy-Coder/uva-uhunt/commit/293387bb1ca547ff5fedc386d810e2432f48b789)) +* **components:data-table:** display `runtime` in live submissions data-table ([b319bfe](https://github.com/Clumsy-Coder/uva-uhunt/commit/b319bfe872d7faa3b3bf79122bb4e878ee023eb4)) +* **components:data-table:** display `submit time` in live submissions data-table ([77e225e](https://github.com/Clumsy-Coder/uva-uhunt/commit/77e225efb8342ea390aae846eeb90d91ef695c01)) +* **components:data-table:** display `username` in live submissions data-table ([3ad7c53](https://github.com/Clumsy-Coder/uva-uhunt/commit/3ad7c53efa4816d41bb288bcfa71284d1d8f06c6)) +* **components:data-table:** display `verdict` in live submissions data-table ([7b57189](https://github.com/Clumsy-Coder/uva-uhunt/commit/7b5718967bc0f16a9cfd4b423fda2a9410534369)) +* **components:data-table:** display submit time using tooltip ([30c02eb](https://github.com/Clumsy-Coder/uva-uhunt/commit/30c02eb561d046316feb77d6674f48e2338a9e2f)) +* **page:home:** display the `DataTable` for live submissions ([fc356b1](https://github.com/Clumsy-Coder/uva-uhunt/commit/fc356b1569ece65edf57c9b5e4c715cf444c9128)) + +## [1.0.0-development.3](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.2...1.0.0-development.3) (2024-1-2) + + +### :sparkles: Features + +* **components:navbar:** highlight active links ([1c21c75](https://github.com/Clumsy-Coder/uva-uhunt/commit/1c21c75854c6374195b82f45b6108a498897f553)) +* **components:** add a Link to the problem page in `DataTable` for `/problems` ([e9c16f5](https://github.com/Clumsy-Coder/uva-uhunt/commit/e9c16f524f70fd656bab601f76c97f8e72eef0e2)) +* **components:** add column properties to be sortable `DataTable` component ([9b1ab5d](https://github.com/Clumsy-Coder/uva-uhunt/commit/9b1ab5ddbca663b0778f25c36a58ab54efaed41d)) +* **page:problems:** display all problems using `DataTable` component ([401ae1c](https://github.com/Clumsy-Coder/uva-uhunt/commit/401ae1c94c4c48299ed8b38e2d451b539f95a87e)) +* **shadcn:data-table:** add button to clear filter ([5072674](https://github.com/Clumsy-Coder/uva-uhunt/commit/5072674429d13a38139811800640a996c3082b8a)) +* **shadcn:data-table:** add column header component for DataTable ([165aa6c](https://github.com/Clumsy-Coder/uva-uhunt/commit/165aa6cf119bc2264698be5088b78ee73a30946a)), closes [/github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/apps/www/app/examples/tasks/components/columns.tsx#L38-L46C4](https://github.com/Clumsy-Coder//github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/apps/www/app/examples/tasks/components/columns.tsx/issues/L38-L46C4) +* **shadcn:data-table:** add loading component for DataTable ([3851747](https://github.com/Clumsy-Coder/uva-uhunt/commit/3851747fdebe98852d772742ee3fc570fddc4da2)) +* **shadcn:** add `DataTablePagination` component ([2500aa1](https://github.com/Clumsy-Coder/uva-uhunt/commit/2500aa18a838a699d2a6b3e2314830e992bc31f2)) +* **shadcn:** add ability to filter any column in `DataTable` component ([5ec2df1](https://github.com/Clumsy-Coder/uva-uhunt/commit/5ec2df1db765eba419fad4233dc14cf508c1ced5)) +* **shadcn:** add column scrolling to `DataTable` component ([55f98b4](https://github.com/Clumsy-Coder/uva-uhunt/commit/55f98b40feea5fcc0651e41b32ae808f571fb8d3)) +* **shadcn:** add column visibility toggle for `DataTable` component ([c623e43](https://github.com/Clumsy-Coder/uva-uhunt/commit/c623e43515f9ed918da1afd6537b81a26cb028f4)) +* **shadcn:** add pagination to `DataTable` component ([2c7df8e](https://github.com/Clumsy-Coder/uva-uhunt/commit/2c7df8e66f51d9c1e567b61dfa745e96b64bacbd)) + +## [1.0.0-development.2](https://github.com/Clumsy-Coder/uva-uhunt/compare/1.0.0-development.1...1.0.0-development.2) (2023-12-31) + + +### :sparkles: Features + +* **api:poll:** process submissions from endpoint `/api/poll/[pollId]` ([ad53d2e](https://github.com/Clumsy-Coder/uva-uhunt/commit/ad53d2eaa94daf3c5df085a8c6bfb57ae5d746b4)) +* **api:** add api endpoint `/api/poll` ([c60046b](https://github.com/Clumsy-Coder/uva-uhunt/commit/c60046ba354413e12cf0b870f0caa133690b8464)) +* **api:** add endpoint `/api/problems/:problemNum` ([62ce762](https://github.com/Clumsy-Coder/uva-uhunt/commit/62ce762f1485825b5d2a4d8f714b504b68ab6484)) +* **api:** add endpoint `/api/problems` ([e65ce18](https://github.com/Clumsy-Coder/uva-uhunt/commit/e65ce18a9dc4881759dd43eca4160cf09f10d24b)) +* **api:** add some basic checks for endpoint `/api/problems/:problemNum` ([94049cc](https://github.com/Clumsy-Coder/uva-uhunt/commit/94049cc303a633836c7e90e40c92a46d7e3f4f8c)) +* **components:** add `Navbar` component ([27c1b8f](https://github.com/Clumsy-Coder/uva-uhunt/commit/27c1b8ff379a5503f72c807feec90fa8447cbfdc)) +* **components:** add component `LiveSubmissionTable` ([0278c98](https://github.com/Clumsy-Coder/uva-uhunt/commit/0278c98c88c1647db38ac38193a0d21258e459ac)) +* **hooks:** add react-query hook to fetch live submissions ([19f0d72](https://github.com/Clumsy-Coder/uva-uhunt/commit/19f0d72402db6d3fa10f4aa32b2ea96afc754ce7)) +* **page:home:** add `Skeleton` when fetching data ([dd8b5b4](https://github.com/Clumsy-Coder/uva-uhunt/commit/dd8b5b4ea3513e94aabfd5f2c7237e0b0ebfa2ab)) +* **page:home:** fetch live submissions and display it ([1d67137](https://github.com/Clumsy-Coder/uva-uhunt/commit/1d671374c2864a1fa3726da841c42cc11d7b2536)) + +## 1.0.0-development.1 (2023-11-05) + + +### :sparkles: Features + +* **component:** add `darkmode-toggle` to main page ([8fa3a3b](https://github.com/Clumsy-Coder/uva-uhunt/commit/8fa3a3bc5c080fe8e79ccf27296947c0d4ad7471)) +* **components:** add component `darkmode-toggle` ([a355c0f](https://github.com/Clumsy-Coder/uva-uhunt/commit/a355c0fd3f88e8a7dc97a0a760c3ce54d06ddfb4)) diff --git a/README.md b/README.md index c403366..be8e827 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# uva-uhunt + +visual redesign of [uva-uhunt](https://uhunt.onlinejudge.org/) + +Using + +- [Shadcn-ui](https://ui.shadcn.com/) +- [Talwindcss](https://tailwindcss.com/) +- [uva uhunt API](https://uhunt.onlinejudge.org/api) +- [Recharts](https://recharts.org/en-US/) +- [zod](https://zod.dev/) + +--- + This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started @@ -16,21 +30,33 @@ bun dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Screenshots + +
+Show screenshots + +### Navbar +![Navbar 1](docs/images/navbar.png) + +### Home page (live submissions) +![Home page 1](docs/images/page-home-loading.png) + +![Home page 2](docs/images/page-home.png) -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +### Problems page (`/problems`) +![Problems page 1](docs/images/page-problems-loading.png) -## Learn More +![Problems page 2](docs/images/page-problems.png) -To learn more about Next.js, take a look at the following resources: +### Problems page (`/problems/[problemNum]`) +![Problems num page 1](docs/images/page-problems-num-loading.png) -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +![Problems num page 2](docs/images/page-problems-num.png) -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +### Username page (`/users/[username]`) +![Username page 1](docs/images/page-users-username-loading.png) -## Deploy on Vercel +![Username page 2](docs/images/page-users-username.png) -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +
diff --git a/components.json b/components.json index 4cfd645..4268381 100644 --- a/components.json +++ b/components.json @@ -4,7 +4,7 @@ "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "zinc", "cssVariables": true @@ -13,4 +13,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/docs/images/navbar.png b/docs/images/navbar.png new file mode 100644 index 0000000..f40256f Binary files /dev/null and b/docs/images/navbar.png differ diff --git a/docs/images/page-home-loading.png b/docs/images/page-home-loading.png new file mode 100644 index 0000000..93898e2 Binary files /dev/null and b/docs/images/page-home-loading.png differ diff --git a/docs/images/page-home.png b/docs/images/page-home.png new file mode 100644 index 0000000..5c99957 Binary files /dev/null and b/docs/images/page-home.png differ diff --git a/docs/images/page-problems-loading.png b/docs/images/page-problems-loading.png new file mode 100644 index 0000000..4f10aa3 Binary files /dev/null and b/docs/images/page-problems-loading.png differ diff --git a/docs/images/page-problems-num-loading.png b/docs/images/page-problems-num-loading.png new file mode 100644 index 0000000..4791786 Binary files /dev/null and b/docs/images/page-problems-num-loading.png differ diff --git a/docs/images/page-problems-num.png b/docs/images/page-problems-num.png new file mode 100644 index 0000000..0fdbe97 Binary files /dev/null and b/docs/images/page-problems-num.png differ diff --git a/docs/images/page-problems.png b/docs/images/page-problems.png new file mode 100644 index 0000000..9f7983d Binary files /dev/null and b/docs/images/page-problems.png differ diff --git a/docs/images/page-users-username-loading.png b/docs/images/page-users-username-loading.png new file mode 100644 index 0000000..08c5ab3 Binary files /dev/null and b/docs/images/page-users-username-loading.png differ diff --git a/docs/images/page-users-username.png b/docs/images/page-users-username.png new file mode 100644 index 0000000..4e3a123 Binary files /dev/null and b/docs/images/page-users-username.png differ diff --git a/package-lock.json b/package-lock.json index fa0e601..18374ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,35 @@ { "name": "uva-uhunt", - "version": "0.1.0", + "version": "1.0.0-development.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uva-uhunt", - "version": "0.1.0", + "version": "1.0.0-development.7", "dependencies": { "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.17.1", + "@tanstack/react-query-devtools": "^5.17.12", + "@tanstack/react-table": "^8.11.6", + "@tanstack/react-virtual": "^3.0.1", + "axios": "^1.6.5", "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", - "lucide-react": "^0.292.0", - "next": "14.0.1", + "clsx": "^2.1.0", + "lucide-react": "^0.309.0", + "moment": "^2.30.1", + "next": "14.0.4", "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", - "tailwind-merge": "^2.0.0", - "tailwindcss-animate": "^1.0.7" + "recharts": "^2.10.4", + "tailwind-merge": "^2.2.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", @@ -29,11 +40,11 @@ "autoprefixer": "^10.0.1", "conventional-changelog": "^5.1.0", "eslint": "^8", - "eslint-config-next": "14.0.1", + "eslint-config-next": "14.0.4", "postcss": "^8", - "semantic-release": "^22.0.7", + "semantic-release": "^22.0.12", "semantic-release-export-data": "^1.0.1", - "tailwindcss": "^3.3.0", + "tailwindcss": "^3.4.1", "typescript": "^5" } }, @@ -256,9 +267,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -301,9 +312,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -324,9 +335,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -461,23 +472,23 @@ } }, "node_modules/@next/env": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.1.tgz", - "integrity": "sha512-Ms8ZswqY65/YfcjrlcIwMPD7Rg/dVjdLapMcSHG26W6O67EJDF435ShW4H4LXi1xKO1oRc97tLXUpx8jpLe86A==" + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", + "integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.1.tgz", - "integrity": "sha512-bLjJMwXdzvhnQOnxvHoTTUh/+PYk6FF/DCgHi4BXwXCINer+o1ZYfL9aVeezj/oI7wqGJOqwGIXrlBvPbAId3w==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.4.tgz", + "integrity": "sha512-U3qMNHmEZoVmHA0j/57nRfi3AscXNvkOnxDmle/69Jz/G0o/gWjXTDdlgILZdrxQ0Lw/jv2mPW8PGy0EGIHXhQ==", "dev": true, "dependencies": { "glob": "7.1.7" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.1.tgz", - "integrity": "sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz", + "integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==", "cpu": [ "arm64" ], @@ -490,9 +501,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.1.tgz", - "integrity": "sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", + "integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", "cpu": [ "x64" ], @@ -505,9 +516,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.1.tgz", - "integrity": "sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", + "integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", "cpu": [ "arm64" ], @@ -520,9 +531,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.1.tgz", - "integrity": "sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", + "integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", "cpu": [ "arm64" ], @@ -535,9 +546,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.1.tgz", - "integrity": "sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", + "integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", "cpu": [ "x64" ], @@ -550,9 +561,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.1.tgz", - "integrity": "sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", + "integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", "cpu": [ "x64" ], @@ -565,9 +576,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.1.tgz", - "integrity": "sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", + "integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", "cpu": [ "arm64" ], @@ -580,9 +591,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.1.tgz", - "integrity": "sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", + "integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", "cpu": [ "ia32" ], @@ -595,9 +606,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.1.tgz", - "integrity": "sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", + "integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", "cpu": [ "x64" ], @@ -830,6 +841,14 @@ "node": ">=12" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -1036,6 +1055,14 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", + "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", @@ -1227,6 +1254,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", + "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1245,6 +1315,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -1315,6 +1419,23 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", @@ -1351,6 +1472,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", @@ -1763,6 +1907,165 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.17.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.10.tgz", + "integrity": "sha512-bJ2oQUDBftvHcEkLS3gyzzShSeZpJyzNNRu8oHK13iNdsofyaDXtNO/c1Zy/PZYVX+PhqOXwoT42gMiEMRSSfQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.17.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.17.7.tgz", + "integrity": "sha512-TfgvOqza5K7Sk6slxqkRIvXlEJoUoPSsGGwpuYSrpqgSwLSSvPPpZhq7hv7hcY5IvRoTNGoq6+MT01C/jILqoQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.17.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.12.tgz", + "integrity": "sha512-mJQ+3da1ug4t9b+GycUuNzMs5hd6t+F4tJ1hqyaN/dlETP+bWwYwrv2GXFIbZIfI1K1Hu+XWZ6HUhRPbNtZ4QQ==", + "dependencies": { + "@tanstack/query-core": "5.17.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.17.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.17.12.tgz", + "integrity": "sha512-5n2oqe9GUD7+QNJqfFm8RqQVVFOeaLQDujBnhyyILDq8XPB6wymTEaS6OJ/CdrhCYKoQe3Uh8E6u9ChABEMuhA==", + "dependencies": { + "@tanstack/query-devtools": "5.17.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.17.12", + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.11.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.6.tgz", + "integrity": "sha512-i0heTVZtuHF9VPMwcYoZ21hbzGDmLjxHYemDMvbGpjk5fUZ8nLF3S8qjVRU79XfPW8KK9o7iTU2fGFVQQmxMSQ==", + "dependencies": { + "@tanstack/table-core": "8.11.6" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", + "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", + "dependencies": { + "@tanstack/virtual-core": "3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.11.6", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.6.tgz", + "integrity": "sha512-69WEY1PaZROaGYUrseng4/4sMYnRGhDe1vM6888CnWekGz/wuCnvqwOoOuKGYivnaiI4BVmZq4WKWhvahyj3/g==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", + "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", + "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1770,9 +2073,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "version": "20.11.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.2.tgz", + "integrity": "sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1791,9 +2094,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.34", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.34.tgz", - "integrity": "sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==", + "version": "18.2.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", + "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -1802,9 +2105,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", - "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "devOptional": true, "dependencies": { "@types/react": "*" @@ -2257,6 +2560,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -2315,6 +2623,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -2548,6 +2866,14 @@ "url": "https://joebell.co.uk" } }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/clean-stack": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", @@ -2610,9 +2936,9 @@ } }, "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "engines": { "node": ">=6" } @@ -2635,6 +2961,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3125,6 +3462,116 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "devOptional": true }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3160,6 +3607,11 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3206,6 +3658,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -3260,6 +3720,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -3471,15 +3939,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3526,12 +3994,12 @@ } }, "node_modules/eslint-config-next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.1.tgz", - "integrity": "sha512-QfIFK2WD39H4WOespjgf6PLv9Bpsd7KGGelCtmq4l67nGvnlsGpuvj0hIT+aIy6p5gKH+lAChYILsyDlxP52yg==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.4.tgz", + "integrity": "sha512-9/xbOHEQOmQtqvQ1UsTQZpnA7SlDMBtuKJ//S4JnoyK3oGLhILKXdBgu/UO7lQo/2xOykQULS1qQ6p2+EpHgAQ==", "dev": true, "dependencies": { - "@next/eslint-plugin-next": "14.0.1", + "@next/eslint-plugin-next": "14.0.4", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", @@ -3893,6 +4361,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -3934,6 +4407,14 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -4081,6 +4562,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4090,6 +4590,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4623,6 +5136,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from-esm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.1.tgz", + "integrity": "sha512-YltaeDglQ6wDZOC8ZAY2I8vK1Ag4XVbs4GhlvNALWz0ee5V+CMkcBhAKbs1iuJZ3fmfgrKFCDRwliM3OxyQMLA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=16.20" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4690,6 +5226,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/into-stream": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", @@ -5387,8 +5931,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", @@ -5456,9 +5999,9 @@ } }, "node_modules/lucide-react": { - "version": "0.292.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.292.0.tgz", - "integrity": "sha512-rRgUkpEHWpa5VCT66YscInCQmQuPCB1RFRzkkxMxg4b+jaL0V12E3riWWR2Sh5OIiUhCwGW/ZExuEO4Az32E6Q==", + "version": "0.309.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.309.0.tgz", + "integrity": "sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -5557,6 +6100,25 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -5589,6 +6151,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5606,9 +6176,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -5641,14 +6211,15 @@ "dev": true }, "node_modules/next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-14.0.1.tgz", - "integrity": "sha512-s4YaLpE4b0gmb3ggtmpmV+wt+lPRuGtANzojMQ2+gmBpgX9w5fTbjsy6dXByBuENsdCX5pukZH/GxdFgO62+pA==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz", + "integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==", "dependencies": { - "@next/env": "14.0.1", + "@next/env": "14.0.4", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", + "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1", "watchpack": "2.4.0" @@ -5660,15 +6231,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.0.1", - "@next/swc-darwin-x64": "14.0.1", - "@next/swc-linux-arm64-gnu": "14.0.1", - "@next/swc-linux-arm64-musl": "14.0.1", - "@next/swc-linux-x64-gnu": "14.0.1", - "@next/swc-linux-x64-musl": "14.0.1", - "@next/swc-win32-arm64-msvc": "14.0.1", - "@next/swc-win32-ia32-msvc": "14.0.1", - "@next/swc-win32-x64-msvc": "14.0.1" + "@next/swc-darwin-arm64": "14.0.4", + "@next/swc-darwin-x64": "14.0.4", + "@next/swc-linux-arm64-gnu": "14.0.4", + "@next/swc-linux-arm64-musl": "14.0.4", + "@next/swc-linux-x64-gnu": "14.0.4", + "@next/swc-linux-x64-musl": "14.0.4", + "@next/swc-win32-arm64-msvc": "14.0.4", + "@next/swc-win32-ia32-msvc": "14.0.4", + "@next/swc-win32-x64-msvc": "14.0.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5695,6 +6266,33 @@ "react-dom": "*" } }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/node-emoji": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.0.tgz", @@ -5945,14 +6543,17 @@ }, "node_modules/npm/node_modules/@colors/colors": { "version": "1.5.0", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.1.90" } }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -5969,6 +6570,7 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -5980,11 +6582,13 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6001,6 +6605,7 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6015,11 +6620,13 @@ }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { "version": "2.2.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6035,6 +6642,7 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/agent-base": { "version": "7.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6046,6 +6654,7 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/http-proxy-agent": { "version": "7.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6058,6 +6667,7 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6070,6 +6680,7 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { "version": "8.0.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6083,6 +6694,7 @@ }, "node_modules/npm/node_modules/@npmcli/arborist": { "version": "7.2.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6129,6 +6741,7 @@ }, "node_modules/npm/node_modules/@npmcli/config": { "version": "8.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6147,6 +6760,7 @@ }, "node_modules/npm/node_modules/@npmcli/disparity-colors": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6158,6 +6772,7 @@ }, "node_modules/npm/node_modules/@npmcli/fs": { "version": "3.1.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6169,6 +6784,7 @@ }, "node_modules/npm/node_modules/@npmcli/git": { "version": "5.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6187,6 +6803,7 @@ }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { "version": "2.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6202,6 +6819,7 @@ }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { "version": "3.0.4", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6216,6 +6834,7 @@ }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { "version": "7.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6230,6 +6849,7 @@ }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6238,6 +6858,7 @@ }, "node_modules/npm/node_modules/@npmcli/node-gyp": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6246,6 +6867,7 @@ }, "node_modules/npm/node_modules/@npmcli/package-json": { "version": "5.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6263,6 +6885,7 @@ }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { "version": "7.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6274,6 +6897,7 @@ }, "node_modules/npm/node_modules/@npmcli/query": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6285,6 +6909,7 @@ }, "node_modules/npm/node_modules/@npmcli/run-script": { "version": "7.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6300,14 +6925,17 @@ }, "node_modules/npm/node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=14" } }, "node_modules/npm/node_modules/@sigstore/bundle": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6319,6 +6947,7 @@ }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { "version": "0.2.1", + "dev": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -6327,6 +6956,7 @@ }, "node_modules/npm/node_modules/@sigstore/sign": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6340,6 +6970,7 @@ }, "node_modules/npm/node_modules/@sigstore/tuf": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6352,6 +6983,7 @@ }, "node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6360,6 +6992,7 @@ }, "node_modules/npm/node_modules/@tufjs/models": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6372,6 +7005,7 @@ }, "node_modules/npm/node_modules/abbrev": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6380,6 +7014,7 @@ }, "node_modules/npm/node_modules/abort-controller": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6391,6 +7026,7 @@ }, "node_modules/npm/node_modules/aggregate-error": { "version": "3.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6403,6 +7039,7 @@ }, "node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6411,6 +7048,7 @@ }, "node_modules/npm/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6425,16 +7063,19 @@ }, "node_modules/npm/node_modules/aproba": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/archy": { "version": "1.0.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/are-we-there-yet": { "version": "4.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6447,11 +7088,13 @@ }, "node_modules/npm/node_modules/balanced-match": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/base64-js": { "version": "1.5.1", + "dev": true, "funding": [ { "type": "github", @@ -6471,6 +7114,7 @@ }, "node_modules/npm/node_modules/bin-links": { "version": "4.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6485,6 +7129,7 @@ }, "node_modules/npm/node_modules/binary-extensions": { "version": "2.2.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6493,6 +7138,7 @@ }, "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6501,6 +7147,7 @@ }, "node_modules/npm/node_modules/buffer": { "version": "6.0.3", + "dev": true, "funding": [ { "type": "github", @@ -6524,6 +7171,7 @@ }, "node_modules/npm/node_modules/builtins": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6532,6 +7180,7 @@ }, "node_modules/npm/node_modules/cacache": { "version": "18.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6554,6 +7203,7 @@ }, "node_modules/npm/node_modules/chalk": { "version": "5.3.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6565,6 +7215,7 @@ }, "node_modules/npm/node_modules/chownr": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6573,6 +7224,7 @@ }, "node_modules/npm/node_modules/ci-info": { "version": "3.9.0", + "dev": true, "funding": [ { "type": "github", @@ -6587,6 +7239,7 @@ }, "node_modules/npm/node_modules/cidr-regex": { "version": "3.1.1", + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -6598,6 +7251,7 @@ }, "node_modules/npm/node_modules/clean-stack": { "version": "2.2.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6606,6 +7260,7 @@ }, "node_modules/npm/node_modules/cli-columns": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6618,6 +7273,7 @@ }, "node_modules/npm/node_modules/cli-table3": { "version": "0.6.3", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6632,6 +7288,7 @@ }, "node_modules/npm/node_modules/clone": { "version": "1.0.4", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6640,6 +7297,7 @@ }, "node_modules/npm/node_modules/cmd-shim": { "version": "6.0.2", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6648,6 +7306,7 @@ }, "node_modules/npm/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6659,11 +7318,13 @@ }, "node_modules/npm/node_modules/color-name": { "version": "1.1.4", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/color-support": { "version": "1.1.3", + "dev": true, "inBundle": true, "license": "ISC", "bin": { @@ -6672,6 +7333,7 @@ }, "node_modules/npm/node_modules/columnify": { "version": "1.6.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6684,16 +7346,19 @@ }, "node_modules/npm/node_modules/common-ancestor-path": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/console-control-strings": { "version": "1.1.0", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/cross-spawn": { "version": "7.0.3", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6707,6 +7372,7 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6721,6 +7387,7 @@ }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -6732,6 +7399,7 @@ }, "node_modules/npm/node_modules/debug": { "version": "4.3.4", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6748,11 +7416,13 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/defaults": { "version": "1.0.4", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6764,11 +7434,13 @@ }, "node_modules/npm/node_modules/delegates": { "version": "1.0.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/diff": { "version": "5.1.0", + "dev": true, "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -6777,24 +7449,29 @@ }, "node_modules/npm/node_modules/eastasianwidth": { "version": "0.2.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/encoding": { "version": "0.1.13", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, "node_modules/npm/node_modules/env-paths": { "version": "2.2.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6803,11 +7480,13 @@ }, "node_modules/npm/node_modules/err-code": { "version": "2.0.3", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/event-target-shim": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6816,6 +7495,7 @@ }, "node_modules/npm/node_modules/events": { "version": "3.3.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6824,11 +7504,13 @@ }, "node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.1", + "dev": true, "inBundle": true, "license": "Apache-2.0" }, "node_modules/npm/node_modules/fastest-levenshtein": { "version": "1.0.16", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6837,6 +7519,7 @@ }, "node_modules/npm/node_modules/foreground-child": { "version": "3.1.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6852,6 +7535,7 @@ }, "node_modules/npm/node_modules/fs-minipass": { "version": "3.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6863,11 +7547,13 @@ }, "node_modules/npm/node_modules/function-bind": { "version": "1.1.1", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/gauge": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6886,6 +7572,7 @@ }, "node_modules/npm/node_modules/glob": { "version": "10.3.10", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6907,11 +7594,13 @@ }, "node_modules/npm/node_modules/graceful-fs": { "version": "4.2.11", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/has": { "version": "1.0.3", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6923,11 +7612,13 @@ }, "node_modules/npm/node_modules/has-unicode": { "version": "2.0.1", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { "version": "7.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6939,13 +7630,16 @@ }, "node_modules/npm/node_modules/http-cache-semantics": { "version": "4.1.1", + "dev": true, "inBundle": true, "license": "BSD-2-Clause" }, "node_modules/npm/node_modules/iconv-lite": { "version": "0.6.3", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -6955,6 +7649,7 @@ }, "node_modules/npm/node_modules/ieee754": { "version": "1.2.1", + "dev": true, "funding": [ { "type": "github", @@ -6974,6 +7669,7 @@ }, "node_modules/npm/node_modules/ignore-walk": { "version": "6.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6985,6 +7681,7 @@ }, "node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6993,6 +7690,7 @@ }, "node_modules/npm/node_modules/indent-string": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7001,6 +7699,7 @@ }, "node_modules/npm/node_modules/ini": { "version": "4.1.1", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7009,6 +7708,7 @@ }, "node_modules/npm/node_modules/init-package-json": { "version": "6.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7026,11 +7726,13 @@ }, "node_modules/npm/node_modules/ip": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/ip-regex": { "version": "4.3.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7039,6 +7741,7 @@ }, "node_modules/npm/node_modules/is-cidr": { "version": "4.0.2", + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7050,6 +7753,7 @@ }, "node_modules/npm/node_modules/is-core-module": { "version": "2.13.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7061,6 +7765,7 @@ }, "node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7069,16 +7774,19 @@ }, "node_modules/npm/node_modules/is-lambda": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { "version": "2.3.6", + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7096,6 +7804,7 @@ }, "node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7104,6 +7813,7 @@ }, "node_modules/npm/node_modules/json-stringify-nice": { "version": "1.1.4", + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -7112,6 +7822,7 @@ }, "node_modules/npm/node_modules/jsonparse": { "version": "1.3.1", + "dev": true, "engines": [ "node >= 0.2.0" ], @@ -7120,16 +7831,19 @@ }, "node_modules/npm/node_modules/just-diff": { "version": "6.0.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/just-diff-apply": { "version": "5.5.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { "version": "8.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7142,6 +7856,7 @@ }, "node_modules/npm/node_modules/libnpmdiff": { "version": "6.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7161,6 +7876,7 @@ }, "node_modules/npm/node_modules/libnpmexec": { "version": "7.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7182,6 +7898,7 @@ }, "node_modules/npm/node_modules/libnpmfund": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7193,6 +7910,7 @@ }, "node_modules/npm/node_modules/libnpmhook": { "version": "10.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7205,6 +7923,7 @@ }, "node_modules/npm/node_modules/libnpmorg": { "version": "6.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7217,6 +7936,7 @@ }, "node_modules/npm/node_modules/libnpmpack": { "version": "6.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7231,6 +7951,7 @@ }, "node_modules/npm/node_modules/libnpmpublish": { "version": "9.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7249,6 +7970,7 @@ }, "node_modules/npm/node_modules/libnpmsearch": { "version": "7.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7260,6 +7982,7 @@ }, "node_modules/npm/node_modules/libnpmteam": { "version": "6.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7272,6 +7995,7 @@ }, "node_modules/npm/node_modules/libnpmversion": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7287,6 +8011,7 @@ }, "node_modules/npm/node_modules/lru-cache": { "version": "10.0.1", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7295,6 +8020,7 @@ }, "node_modules/npm/node_modules/make-fetch-happen": { "version": "13.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7316,6 +8042,7 @@ }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7330,6 +8057,7 @@ }, "node_modules/npm/node_modules/minipass": { "version": "7.0.4", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7338,6 +8066,7 @@ }, "node_modules/npm/node_modules/minipass-collect": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7349,6 +8078,7 @@ }, "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7360,6 +8090,7 @@ }, "node_modules/npm/node_modules/minipass-fetch": { "version": "3.0.4", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7376,6 +8107,7 @@ }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7387,6 +8119,7 @@ }, "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7398,6 +8131,7 @@ }, "node_modules/npm/node_modules/minipass-json-stream": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7407,6 +8141,7 @@ }, "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7418,6 +8153,7 @@ }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7429,6 +8165,7 @@ }, "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7440,6 +8177,7 @@ }, "node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7451,6 +8189,7 @@ }, "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7462,6 +8201,7 @@ }, "node_modules/npm/node_modules/minizlib": { "version": "2.1.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7474,6 +8214,7 @@ }, "node_modules/npm/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7485,6 +8226,7 @@ }, "node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -7496,11 +8238,13 @@ }, "node_modules/npm/node_modules/ms": { "version": "2.1.3", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/mute-stream": { "version": "1.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7509,6 +8253,7 @@ }, "node_modules/npm/node_modules/negotiator": { "version": "0.6.3", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7517,6 +8262,7 @@ }, "node_modules/npm/node_modules/node-gyp": { "version": "10.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7540,6 +8286,7 @@ }, "node_modules/npm/node_modules/nopt": { "version": "7.2.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7554,6 +8301,7 @@ }, "node_modules/npm/node_modules/normalize-package-data": { "version": "6.0.0", + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7568,6 +8316,7 @@ }, "node_modules/npm/node_modules/npm-audit-report": { "version": "5.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7576,6 +8325,7 @@ }, "node_modules/npm/node_modules/npm-bundled": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7587,6 +8337,7 @@ }, "node_modules/npm/node_modules/npm-install-checks": { "version": "6.3.0", + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7598,6 +8349,7 @@ }, "node_modules/npm/node_modules/npm-normalize-package-bin": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7606,6 +8358,7 @@ }, "node_modules/npm/node_modules/npm-package-arg": { "version": "11.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7620,6 +8373,7 @@ }, "node_modules/npm/node_modules/npm-packlist": { "version": "8.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7631,6 +8385,7 @@ }, "node_modules/npm/node_modules/npm-pick-manifest": { "version": "9.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7645,6 +8400,7 @@ }, "node_modules/npm/node_modules/npm-profile": { "version": "9.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7657,6 +8413,7 @@ }, "node_modules/npm/node_modules/npm-registry-fetch": { "version": "16.1.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7674,6 +8431,7 @@ }, "node_modules/npm/node_modules/npm-user-validate": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "engines": { @@ -7682,6 +8440,7 @@ }, "node_modules/npm/node_modules/npmlog": { "version": "7.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7696,6 +8455,7 @@ }, "node_modules/npm/node_modules/p-map": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7710,6 +8470,7 @@ }, "node_modules/npm/node_modules/pacote": { "version": "17.0.4", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7741,6 +8502,7 @@ }, "node_modules/npm/node_modules/parse-conflict-json": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7754,6 +8516,7 @@ }, "node_modules/npm/node_modules/path-key": { "version": "3.1.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7762,6 +8525,7 @@ }, "node_modules/npm/node_modules/path-scurry": { "version": "1.10.1", + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7777,6 +8541,7 @@ }, "node_modules/npm/node_modules/postcss-selector-parser": { "version": "6.0.13", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7789,6 +8554,7 @@ }, "node_modules/npm/node_modules/proc-log": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7797,6 +8563,7 @@ }, "node_modules/npm/node_modules/process": { "version": "0.11.10", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7805,6 +8572,7 @@ }, "node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -7813,6 +8581,7 @@ }, "node_modules/npm/node_modules/promise-call-limit": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -7821,11 +8590,13 @@ }, "node_modules/npm/node_modules/promise-inflight": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/promise-retry": { "version": "2.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7838,6 +8609,7 @@ }, "node_modules/npm/node_modules/promzard": { "version": "1.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7849,6 +8621,7 @@ }, "node_modules/npm/node_modules/qrcode-terminal": { "version": "0.12.0", + "dev": true, "inBundle": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" @@ -7856,6 +8629,7 @@ }, "node_modules/npm/node_modules/read": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7867,6 +8641,7 @@ }, "node_modules/npm/node_modules/read-cmd-shim": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7875,6 +8650,7 @@ }, "node_modules/npm/node_modules/read-package-json": { "version": "7.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7889,6 +8665,7 @@ }, "node_modules/npm/node_modules/read-package-json-fast": { "version": "3.0.2", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7901,6 +8678,7 @@ }, "node_modules/npm/node_modules/readable-stream": { "version": "4.4.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7916,6 +8694,7 @@ }, "node_modules/npm/node_modules/retry": { "version": "0.12.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7924,6 +8703,7 @@ }, "node_modules/npm/node_modules/safe-buffer": { "version": "5.2.1", + "dev": true, "funding": [ { "type": "github", @@ -7943,11 +8723,14 @@ }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/npm/node_modules/semver": { "version": "7.5.4", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7962,6 +8745,7 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7973,11 +8757,13 @@ }, "node_modules/npm/node_modules/set-blocking": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/shebang-command": { "version": "2.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7989,6 +8775,7 @@ }, "node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7997,6 +8784,7 @@ }, "node_modules/npm/node_modules/signal-exit": { "version": "4.0.2", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8008,6 +8796,7 @@ }, "node_modules/npm/node_modules/sigstore": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8022,6 +8811,7 @@ }, "node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8031,6 +8821,7 @@ }, "node_modules/npm/node_modules/socks": { "version": "2.7.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8044,6 +8835,7 @@ }, "node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8053,11 +8845,13 @@ }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.3.0", + "dev": true, "inBundle": true, "license": "CC-BY-3.0" }, "node_modules/npm/node_modules/spdx-expression-parse": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8067,11 +8861,13 @@ }, "node_modules/npm/node_modules/spdx-license-ids": { "version": "3.0.16", + "dev": true, "inBundle": true, "license": "CC0-1.0" }, "node_modules/npm/node_modules/ssri": { "version": "10.0.5", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8083,6 +8879,7 @@ }, "node_modules/npm/node_modules/string_decoder": { "version": "1.3.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8091,6 +8888,7 @@ }, "node_modules/npm/node_modules/string-width": { "version": "4.2.3", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8105,6 +8903,7 @@ "node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8118,6 +8917,7 @@ }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8130,6 +8930,7 @@ "node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8141,6 +8942,7 @@ }, "node_modules/npm/node_modules/supports-color": { "version": "9.4.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8152,6 +8954,7 @@ }, "node_modules/npm/node_modules/tar": { "version": "6.2.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8168,6 +8971,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8179,6 +8983,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8190,6 +8995,7 @@ }, "node_modules/npm/node_modules/tar/node_modules/minipass": { "version": "5.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8198,16 +9004,19 @@ }, "node_modules/npm/node_modules/text-table": { "version": "0.2.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/tiny-relative-date": { "version": "1.3.0", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8216,6 +9025,7 @@ }, "node_modules/npm/node_modules/tuf-js": { "version": "2.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8229,6 +9039,7 @@ }, "node_modules/npm/node_modules/unique-filename": { "version": "3.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8240,6 +9051,7 @@ }, "node_modules/npm/node_modules/unique-slug": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8251,11 +9063,13 @@ }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/validate-npm-package-license": { "version": "3.0.4", + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8265,6 +9079,7 @@ }, "node_modules/npm/node_modules/validate-npm-package-name": { "version": "5.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8276,11 +9091,13 @@ }, "node_modules/npm/node_modules/walk-up-path": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/wcwidth": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8289,6 +9106,7 @@ }, "node_modules/npm/node_modules/which": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8303,6 +9121,7 @@ }, "node_modules/npm/node_modules/which/node_modules/isexe": { "version": "3.1.1", + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8311,6 +9130,7 @@ }, "node_modules/npm/node_modules/wide-align": { "version": "1.1.5", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8319,6 +9139,7 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8336,6 +9157,7 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8352,6 +9174,7 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8363,6 +9186,7 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8374,11 +9198,13 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8395,6 +9221,7 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8409,6 +9236,7 @@ }, "node_modules/npm/node_modules/write-file-atomic": { "version": "5.0.1", + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8421,6 +9249,7 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", + "dev": true, "inBundle": true, "license": "ISC" }, @@ -8908,9 +9737,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "funding": [ { "type": "opencollective", @@ -8926,7 +9755,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -9050,7 +9879,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9063,6 +9891,11 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9141,8 +9974,12 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-remove-scroll": { "version": "2.5.5", @@ -9189,6 +10026,20 @@ } } }, + "node_modules/react-smooth": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", + "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", + "dependencies": { + "fast-equals": "^5.0.0", + "react-transition-group": "2.9.0" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -9211,6 +10062,21 @@ } } }, + "node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9338,6 +10204,37 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.4.tgz", + "integrity": "sha512-/Q7/wdf8bW91lN3NEeCjL9RWfaiXQViJFgdnas4Eix/I8B9HAI3tHHK/CW/zDfgRMh4fzW1zlfjoz1IAapLO1Q==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-smooth": "^2.0.5", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, "node_modules/redeyed": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", @@ -9537,9 +10434,9 @@ } }, "node_modules/semantic-release": { - "version": "22.0.7", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.7.tgz", - "integrity": "sha512-Stx23Hjn7iU8GOAlhG3pHlR7AoNEahj9q7lKBP0rdK2BasGtJ4AWYh3zm1u3SCMuFiA8y4CE/Gu4RGKau1WiaQ==", + "version": "22.0.12", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", + "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", @@ -9558,6 +10455,7 @@ "git-log-parser": "^1.2.0", "hook-std": "^3.0.0", "hosted-git-info": "^7.0.0", + "import-from-esm": "^1.3.1", "lodash-es": "^4.17.21", "marked": "^9.0.0", "marked-terminal": "^6.0.0", @@ -10155,11 +11053,11 @@ } }, "node_modules/tailwind-merge": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.0.0.tgz", - "integrity": "sha512-WO8qghn9yhsldLSg80au+3/gY9E4hFxIvQ3qOmlpXnqpDKoMruKfi/56BbbMg6fHTQJ9QD3cc79PoWqlaQE4rw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", + "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", "dependencies": { - "@babel/runtime": "^7.23.1" + "@babel/runtime": "^7.23.5" }, "funding": { "type": "github", @@ -10167,9 +11065,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", - "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10311,6 +11209,11 @@ "xtend": "~4.0.1" } }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10464,9 +11367,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -10674,6 +11577,27 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.7.0", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", + "integrity": "sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -10875,6 +11799,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 141824e..989f4be 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,38 @@ { "name": "uva-uhunt", - "version": "0.1.0", + "version": "1.0.0-development.7", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "npm run lint:tsc && npm run lint:eslint", + "lint:tsc": "tsc --build tsconfig.json .", + "lint:eslint": "next lint" }, "dependencies": { "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.17.1", + "@tanstack/react-query-devtools": "^5.17.12", + "@tanstack/react-table": "^8.11.6", + "@tanstack/react-virtual": "^3.0.1", + "axios": "^1.6.5", "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", - "lucide-react": "^0.292.0", - "next": "14.0.1", + "clsx": "^2.1.0", + "lucide-react": "^0.309.0", + "moment": "^2.30.1", + "next": "14.0.4", "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", - "tailwind-merge": "^2.0.0", - "tailwindcss-animate": "^1.0.7" + "recharts": "^2.10.4", + "tailwind-merge": "^2.2.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", @@ -30,11 +43,11 @@ "autoprefixer": "^10.0.1", "conventional-changelog": "^5.1.0", "eslint": "^8", - "eslint-config-next": "14.0.1", + "eslint-config-next": "14.0.4", "postcss": "^8", - "semantic-release": "^22.0.7", + "semantic-release": "^22.0.12", "semantic-release-export-data": "^1.0.1", - "tailwindcss": "^3.3.0", + "tailwindcss": "^3.4.1", "typescript": "^5" } } diff --git a/src/app/api/poll/[pollId]/route.ts b/src/app/api/poll/[pollId]/route.ts new file mode 100644 index 0000000..b78df8a --- /dev/null +++ b/src/app/api/poll/[pollId]/route.ts @@ -0,0 +1,101 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import axios from "axios"; + +import { Language, Problem, Submission, ProblemVerdictMap } from "@/types"; +import { fetchLiveSubmissionsUrl, uhuntProblemIdUrl } from "@/utils/constants"; + +type getParamsType = { + params: { + pollId: string; + }; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + const schema = z.object({ + pollId: z.coerce + .number({ invalid_type_error: "Must be a number" }) + .min(0, "Must be a number greater than 0"), + }); + + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + const { pollId } = params; + + const url = fetchLiveSubmissionsUrl(+pollId); + + const fetchResponse = await axios.get(url) + const data = fetchResponse.data + + const converted = data + .map(async (submission: Submission) => { + submission.msg.verdict = ProblemVerdictMap[submission.msg.ver] || { + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgColor: "bg-gray-500", + title: "- In Queue -", + fgHex: "", + bgHex: "6b7280", + }; + submission.msg.lan = Language[submission.msg.lan] || "--"; + + // add problem number to the object + const pidUrl = uhuntProblemIdUrl(`${submission.msg.pid}`); + const pidResponse = await fetch(pidUrl); + const pidData: Problem = await pidResponse.json(); + submission.msg.pnum = pidData.num; + submission.msg.pTitle = pidData.title; + + submission.msg.rank = + +submission.msg.rank < 0 ? "-" : submission.msg.rank; + + return submission; + }) + .reverse(); + + // using Promise.all because the map function is using an async function + // use Promise.all to resolver async operations + // obtained from: https://stackoverflow.com/a/68279968/3053548 + await Promise.all(converted); + + // the latest submission is the last element. so reversing it will give the latest submission first + let processedSubmission = data.reverse(); + // group by submission id + // there are multiple elements with the same submission ID. + // The first one is usually indicating `in queue`. + // the second one is a verdict of the submission + const groupBySubmission = Array.from( + data + .reduce( + (entryMap, submission) => + entryMap.set(submission.msg.sid, [ + ...(entryMap.get(submission.msg.sid) || []), + submission, + ]), + new Map(), + ) + .values(), + ); + + // take the latest submission from grouped array elements + // the grouped submissions will have one element with the verdict of `in queue` and the other final verdict + const flattenedSubmission = groupBySubmission.reduce( + (result: Submission[], value, key) => [ + ...result, + { ...value[0], msg: value[0].msg }, + ], + [], + ); + //sort submission by submission time in descending order + flattenedSubmission.sort((a, b) => b.msg.sbt - a.msg.sbt); + + return Response.json(flattenedSubmission); +}; diff --git a/src/app/api/problems/[problemNum]/route.ts b/src/app/api/problems/[problemNum]/route.ts new file mode 100644 index 0000000..fc3ce22 --- /dev/null +++ b/src/app/api/problems/[problemNum]/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { uhuntProblemNumUrl } from "@/utils/constants"; +import { Problem, ProblemStatus } from "@/types"; +import { problemNumSchema as schema } from "@/schema"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + const { problemNum } = params; + + const url = uhuntProblemNumUrl(problemNum); + + const response = await fetch(url); + const data: Problem = await response.json(); + + if(Object.entries(data).length === 0) { + const message = { + message: `Problem number ${problemNum} not found` + } + return NextResponse.json(message, { + status: 404, + }); + } + + data.status = ProblemStatus[data.status] + + return Response.json(data); +}; diff --git a/src/app/api/problems/ranklist/[problemNum]/route.ts b/src/app/api/problems/ranklist/[problemNum]/route.ts new file mode 100644 index 0000000..d84611d --- /dev/null +++ b/src/app/api/problems/ranklist/[problemNum]/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { problemNumRanklistSchema as schema } from "@/schema"; +import { Language, Problem, ProblemVerdictMap, Submission } from "@/types"; +import { uhuntProblemNumUrl, uhuntProblemRankUrl } from "@/utils/constants"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch problem stats + const { problemNum } = params; + + const problemUrl = uhuntProblemNumUrl(problemNum); + const problemResponse = await fetch(problemUrl); + const problemData: Problem = await problemResponse.json(); + + // return 404 if problem doesn't exist + if (Object.entries(problemData).length === 0) { + const message = { + message: `Problem number ${problemNum} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch problem ranklist + const ranklistUrl = uhuntProblemRankUrl(problemData.pid, 1, 10); + const ranklistResponse = await fetch(ranklistUrl); + const ranklistData: Submission["msg"][] = await ranklistResponse.json(); + + // add properties to the ranklist array + const converted = ranklistData.map((rank: Submission["msg"]) => { + rank.verdict = ProblemVerdictMap[rank.ver] || { + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgColor: "bg-gray-500", + title: "- In Queue -", + fgHex: "", + bgHex: "6b7280", + }; + rank.lan = Language[rank.lan] || "--"; + rank.pnum = problemData.num; + rank.pTitle = problemData.title; + + return rank; + }); + + return Response.json(converted); +}; diff --git a/src/app/api/problems/route.ts b/src/app/api/problems/route.ts new file mode 100644 index 0000000..9779e8d --- /dev/null +++ b/src/app/api/problems/route.ts @@ -0,0 +1,79 @@ +import { uhuntAllProblemsUrl } from "@/utils/constants"; +import { Problem, ProblemStatus } from "@/types"; + +/** + * an array of keys that will be used to convert an array of arrays into array of objects + * + * check `https://uhunt.onlinejudge.org/api/p` to view raw data. (each problem stats are presented as an array element) + */ +const arrKey = [ + "pid", // problem ID + "num", // problem number + "title", // problem title + "dacu", // Number of Distinct Accepted User (DACU) + "mrun", // Best Runtime of an Accepted Submission + "mmem", // Best Memory used of an Accepted Submission + "nover", // Number of No Verdict Given (can be ignored) + "sube", // Number of Submission Error + "noj", // Number of Can't be Judged + "inq", // Number of In Queue + "ce", // Number of Compilation Error + "rf", // Number of Restricted Function + "re", // Number of Runtime Error + "ole", // Number of Output Limit Exceeded + "tle", // Number of Time Limit Exceeded + "mle", // Number of Memory Limit Exceeded + "wa", // Number of Wrong Answer + "pe", // Number of Presentation Error + "ac", // Number of Accepted + "rtl", // Problem Run-Time Limit (milliseconds) + "status", // Problem Status (0 = unavailable, 1 = normal, 2 = special judge) + "rej", +]; + +export const GET = async (_request: Request) => { + const url = uhuntAllProblemsUrl(); + + const response = await fetch(url); + // data returned is an array of array + // ex: + // [ + // [ + // 36, + // 100, + // "The 3n + 1 problem", + // ... + // ], + // ] + const data = await response.json(); + + // convert an array of array into array of problems + // ex: + // convert + // [ + // [1,2,3,...], + // [1,2,3,...], + // ] + // + // into + // [ + // {pid: 1, num: 2, title: 3, ...}, + // {pid: 1, num: 2, title: 3, ...}, + // ] + const converted = data + .map((problem: number[]) => { + const initialValue = {}; + return problem.reduce((obj, item, index: number) => { + return { + ...obj, + [arrKey[index]]: item, + }; + }, initialValue); + }) + .map((problem: Problem) => { + problem.status = ProblemStatus[problem.status]; + return problem; + }); + + return Response.json(converted); +}; diff --git a/src/app/api/search/[searchStr]/route.ts b/src/app/api/search/[searchStr]/route.ts new file mode 100644 index 0000000..9196682 --- /dev/null +++ b/src/app/api/search/[searchStr]/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { searchSchema as schema } from "@/schema"; +import { + uhuntProblemNumUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; +import { Problem, SearchResultType } from "@/types"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async(_request: Request, {params}: getParamsType) => { + const { searchStr } = params; + const responseData: SearchResultType[] = [] + + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const usernameUrl = uhuntUsername2UidUrl(searchStr); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // add search result to array if user exists + if (usernameData !== 0) { + responseData.push({ + title: `User: '${searchStr}'`, + href: `/users/${searchStr}` + }) + } + + //----------------------------------------------------------------------------------------------// + + // fetch problem number + const problemNumUrl = uhuntProblemNumUrl(searchStr) + const problemNumResponse = await fetch(problemNumUrl) + const problemNumData: Problem = await problemNumResponse.json() + + if(Object.entries(problemNumData).length !== 0) { + const {num, title} = problemNumData + responseData.push({ + title: `Problem: ${num} '${title}'`, + href: `/problems/${num}` + }) + } + + return Response.json(responseData) + +} diff --git a/src/app/api/submissions/[problemNum]/route.ts b/src/app/api/submissions/[problemNum]/route.ts new file mode 100644 index 0000000..560683d --- /dev/null +++ b/src/app/api/submissions/[problemNum]/route.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { problemNumSubmissionSchema as schema } from "@/schema"; +import { NextResponse } from "next/server"; +import { + uhuntProblemNumUrl, + uhuntProblemRankUrl, + uhuntProblemSubmissionListUrl, +} from "@/utils/constants"; +import { Language, Problem, ProblemVerdictMap, Submission } from "@/types"; +import moment from "moment"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch problem stats + const { problemNum } = params; + + const problemUrl = uhuntProblemNumUrl(problemNum); + const problemResponse = await fetch(problemUrl); + const problemData: Problem = await problemResponse.json(); + + // return 404 if problem doesn't exist + if (Object.entries(problemData).length === 0) { + const message = { + message: `Problem number ${problemNum} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch submissions of the problem + const submissionsUrl = uhuntProblemSubmissionListUrl( + problemData.pid, + moment().subtract(1, "years").unix(), + moment().unix(), + 500 + ); + const submissionResponse = await fetch(submissionsUrl, { cache: "no-cache" }); + const submissionData: Submission["msg"][] = await submissionResponse.json(); + + const converted = submissionData.map((rank: Submission["msg"]) => { + rank.verdict = ProblemVerdictMap[rank.ver] || { + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgColor: "bg-gray-500", + title: "- In Queue -", + fgHex: "", + bgHex: "6b7280", + }; + rank.lan = Language[rank.lan] || "--"; + rank.pnum = problemData.num; + rank.pTitle = problemData.title; + + return rank; + }); + + return Response.json(converted); +}; diff --git a/src/app/api/submissions/language/[problemNum]/route.tsx b/src/app/api/submissions/language/[problemNum]/route.tsx new file mode 100644 index 0000000..4f17d19 --- /dev/null +++ b/src/app/api/submissions/language/[problemNum]/route.tsx @@ -0,0 +1,85 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { submissionLangSchema as schema } from "@/schema"; +import { + uhuntProblemNumUrl, + uhuntProblemSubmissionListUrl, +} from "@/utils/constants"; +import { Language, Problem, Submission, SubmissionLangType } from "@/types"; + +type getParamsType = { + params: z.infer; +}; + +export type getResponseType = Record; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch problem stats + const { problemNum } = params; + + const problemUrl = uhuntProblemNumUrl(problemNum); + const problemResponse = await fetch(problemUrl); + const problemData: Problem = await problemResponse.json(); + + // return 404 if problem doesn't exist + if (Object.entries(problemData).length === 0) { + const message = { + message: `Problem number ${problemNum} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch submissions of the problem + const submissionsUrl = uhuntProblemSubmissionListUrl(problemData.pid); + const submissionResponse = await fetch(submissionsUrl, { cache: "no-cache" }); + const submissionData: Submission["msg"][] = await submissionResponse.json(); + + // map language id as key and 0 as value. (the value will be count of a submission language) + const languageObj = Object.keys(Language).reduce( + (acc: Record, cur: string) => { + acc[cur] = 0; + + return acc; + }, + {}, + ); + + // increment count of key-value for their respective language ID + const reducedData: Record = submissionData.reduce((acc, cur) => { + const languageId = cur.lan; + acc[languageId] = acc[languageId] + 1; + + return acc; + }, languageObj); + delete reducedData["undefined"]; + + const processedData: SubmissionLangType[] = Object.entries(reducedData).map( + ([key, value]) => { + return { + language: Language[key], + count: value, + }; + }, + ); + + return Response.json(processedData); +}; diff --git a/src/app/api/submissions/overtime/[problemNum]/route.ts b/src/app/api/submissions/overtime/[problemNum]/route.ts new file mode 100644 index 0000000..de01da4 --- /dev/null +++ b/src/app/api/submissions/overtime/[problemNum]/route.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; + +import { submissionOvertimeSchema as schema } from "@/schema"; +import { NextResponse } from "next/server"; +import { uhuntProblemNumUrl, uhuntSubmissionCountUrl } from "@/utils/constants"; +import { Problem, SubmissionsOvertimeLineChartType } from "@/types"; +import moment, { Moment } from "moment"; + +type getParamsType = { + params: z.infer; +}; + +/** + * Get the submission count of a problem using `problem number` + * The submission count will be a cumulative submission count + * + * if invalid `problem number` is given, a response of 400 will be returned + * if the problem doesn't exist, a response of 400 will be returned + */ +export const GET = async (_request: Request, { params }: getParamsType) => { + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + const { problemNum } = params; + + const problemUrl = uhuntProblemNumUrl(problemNum); + const problemResponse = await fetch(problemUrl); + const problemData: Problem = await problemResponse.json(); + + if (Object.entries(problemData).length === 0) { + const message = { + message: `Problem number ${problemNum} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + const submssionCountUrl = uhuntSubmissionCountUrl(problemData.pid); + const submissionResponse = await fetch(submssionCountUrl); + const submissionData: number[] = await submissionResponse.json(); + + // sum the submission count + // each element is a cumulative sum of the previous + const data = submissionData.reduce( + (acc: number[], cur: number, i) => + i === 0 ? [cur] : [...acc, acc[acc.length - 1] + cur], + [], + ); + // map each element to the year. + // use values from the url + // Ex: https://uhunt.onlinejudge.org/api/p/count/36/1704664602/20/12 + // 36 : pid + // 1704664602 : the time to look back on. usually the current time (unix time). Look back from this time + // 20 : return number of years of submission. in this case 20 years + // 12 : Number of months each array element will represent + const submissionUrlSplit = submssionCountUrl.split("/"); + const thirtyDaysInSeconds = 60 * 60 * 24 * 30; + const responseData:SubmissionsOvertimeLineChartType[] = data.map((cur, i) => { + const submissionTime = +submissionUrlSplit[7]; + const back = +submissionUrlSplit[8]; + const jump = +submissionUrlSplit[9]; + const unixTime = submissionTime - (back - i) * thirtyDaysInSeconds * jump; + + return { + name: "submissions", + time: moment.unix(unixTime).format("YYYY"), + submissions: cur, + fill: "#8884d8" + }; + }); + + return Response.json(responseData); +}; diff --git a/src/app/api/users/[username]/submissions/attempted/route.ts b/src/app/api/users/[username]/submissions/attempted/route.ts new file mode 100644 index 0000000..0af57d8 --- /dev/null +++ b/src/app/api/users/[username]/submissions/attempted/route.ts @@ -0,0 +1,94 @@ + +import { z } from "zod"; + +import { NextResponse } from "next/server"; + +import { userSchema as schema } from "@/schema"; +import { + Language, + ProblemVerdictMap, + SubmissionLangType, + SubmissionsOvertimeLineChartType, + SubmissionSovledVsAttempted +} from "@/types"; +import { RawUserSubmission } from "@/types/raw"; +import { + uhuntUserSubmissionsUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; +import moment from "moment"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, {params}: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const { username } = params; + + const usernameUrl = uhuntUsername2UidUrl(username); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // return 404 if problem doesn't exist + if (usernameData === 0) { + const message = { + message: `Username ${username} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + // fetch submissions of the user + const userSubmissionsUrl = uhuntUserSubmissionsUrl(usernameData); + const userSubmissionResponse = await fetch(userSubmissionsUrl, { + cache: "no-cache", + }); + const userSubmissionData = + (await userSubmissionResponse.json()) as RawUserSubmission; + + // count solved submissions + // use a set to keep track of problems solved + const set = new Set(); + + const verdictSolvedId = 90 + userSubmissionData.subs.forEach(cur => { + // cur[2] : verdict ID + if(cur[2] === verdictSolvedId) { + set.add(cur[1]) // add problem ID + } + }) + + const processedData: SubmissionSovledVsAttempted[] = [ + { + name: "Solved", + count: set.size, + tooltipTitle: "Solved unique problems", + fill: ProblemVerdictMap.ac.bgHex + }, + { + name: "Attempts", + count: userSubmissionData.subs.length, + tooltipTitle: "Submission attempts", + fill: ProblemVerdictMap.inq.bgHex + } + ] + + return Response.json(processedData) +} diff --git a/src/app/api/users/[username]/submissions/language/route.ts b/src/app/api/users/[username]/submissions/language/route.ts new file mode 100644 index 0000000..10b07d1 --- /dev/null +++ b/src/app/api/users/[username]/submissions/language/route.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; + +import { NextResponse } from "next/server"; + +import { userSchema as schema } from "@/schema"; +import { Language, SubmissionLangType } from "@/types"; +import { RawUserSubmission } from "@/types/raw"; +import { + uhuntUserSubmissionsUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const { username } = params; + + const usernameUrl = uhuntUsername2UidUrl(username); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // return 404 if problem doesn't exist + if (usernameData === 0) { + const message = { + message: `Username ${username} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch submissions of the user + const userSubmissionsUrl = uhuntUserSubmissionsUrl(usernameData); + const userSubmissionResponse = await fetch(userSubmissionsUrl, { + cache: "no-cache", + }); + const userSubmissionData = + (await userSubmissionResponse.json()) as RawUserSubmission; + + // map language id as key and 0 as value. (the value will be count of a submission language) + let languageObj = Object.keys(Language).reduce( + (acc: Record, cur: string) => { + acc[cur] = 0; + + return acc; + }, + {}, + ); + + // count the submissions by language id + // object key: language ID + // object value: count + const reducedData = userSubmissionData.subs.reduce((acc, cur) => { + const languageId = cur[5]; + acc[languageId] = acc[languageId] + 1; + + return acc; + }, languageObj); + delete reducedData["undefined"]; + + const processedData: SubmissionLangType[] = Object.entries(reducedData).map( + ([key, value]) => { + return { + language: Language[key], + count: value, + }; + }, + ); + + return Response.json(processedData); +}; diff --git a/src/app/api/users/[username]/submissions/overtime/route.ts b/src/app/api/users/[username]/submissions/overtime/route.ts new file mode 100644 index 0000000..c1e73c4 --- /dev/null +++ b/src/app/api/users/[username]/submissions/overtime/route.ts @@ -0,0 +1,124 @@ +import { z } from "zod"; + +import { NextResponse } from "next/server"; + +import { userSchema as schema } from "@/schema"; +import { + Language, + SubmissionLangType, + SubmissionsOvertimeLineChartType, +} from "@/types"; +import { RawUserSubmission } from "@/types/raw"; +import { + uhuntUserSubmissionsUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; +import moment from "moment"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const { username } = params; + + const usernameUrl = uhuntUsername2UidUrl(username); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // return 404 if problem doesn't exist + if (usernameData === 0) { + const message = { + message: `Username ${username} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + // fetch submissions of the user + const userSubmissionsUrl = uhuntUserSubmissionsUrl(usernameData); + const userSubmissionResponse = await fetch(userSubmissionsUrl, { + cache: "no-cache", + }); + const userSubmissionData = + (await userSubmissionResponse.json()) as RawUserSubmission; + + // count submissions by year + const initData: Record = {}; + let reducedData = userSubmissionData.subs.reduce((obj, cur, index) => { + const year = moment.unix(cur[4]).format("YYYY"); + obj[year] = obj[year] + 1 || 1; + + return obj; + }, initData); + + // add current year if it doesn't exist + // this to make sure the chart draws a lint upto the current year + const curYear = new Date().getFullYear() + if(reducedData[curYear] === undefined) { + reducedData[curYear] = 0 + } + + // add missing years + // - loop through the array starting from index 1 going upto last array element + // - convert processedData into an array using Object.entries + // - sort the array + // - get the current element and previous element + // - loop through the difference between curYear and prevYear + // - add entry to `processedData` + for (let i = 1; i < Object.entries(reducedData).length; i++) { + const sorted = Object.entries(reducedData).sort((a, b) => +a[0] - +b[0]); + const curYear = +sorted[i][0]; + const prevYear = +sorted[i - 1][0]; + + // if (curYear - prevYear > 1) { + for (let k = prevYear + 1; k < curYear; k++) { + reducedData[k] = 0; + } + // } + } + + // calculate cumulative sum + for (let i = 1; i < Object.entries(reducedData).length; i++) { + const sorted = Object.entries(reducedData).sort((a, b) => +a[0] - +b[0]); + const [curYear, curYearCount] = sorted[i]; + const [prevYear, prevYearCount] = sorted[i - 1]; + + reducedData[curYear] = curYearCount + prevYearCount; + } + + // construct data structure for Rechart area/line chart + const initProcessedData: SubmissionsOvertimeLineChartType[] = []; + const processedData: SubmissionsOvertimeLineChartType[] = Object.entries( + reducedData, + ).reduce((obj, cur, index) => { + const [year, count] = cur; + obj.push({ + name: "submissions", + time: year, + submissions: count, + fill: "#8884d8", + }); + + return obj; + }, initProcessedData); + + return Response.json(processedData); +}; diff --git a/src/app/api/users/[username]/submissions/route.ts b/src/app/api/users/[username]/submissions/route.ts new file mode 100644 index 0000000..9c33693 --- /dev/null +++ b/src/app/api/users/[username]/submissions/route.ts @@ -0,0 +1,118 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { userSchema as schema } from "@/schema"; +import { + uhuntAllProblemsUrl, + uhuntUserSubmissionsUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; +import { Language, ProblemVerdictMap, UserSub, UserSubmission } from "@/types"; +import { RawProblem, RawUserSubmission } from "@/types/raw"; + +type getParamsType = { + params: z.infer; +}; + +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const { username } = params; + + const usernameUrl = uhuntUsername2UidUrl(username); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // return 404 if problem doesn't exist + if (usernameData === 0) { + const message = { + message: `Username ${username} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch all problems + // this is needed because later each submission will need problem number and problem title. + // if repeatedly fetching from upstream api server, it will slow down. + const allProblemsUrl = uhuntAllProblemsUrl(); + const allProblemsResponse = await fetch(allProblemsUrl); + const allProblemsData: RawProblem[] = await allProblemsResponse.json(); + + // fetch submissions of the user + const userSubmissionsUrl = uhuntUserSubmissionsUrl(usernameData); + const userSubmissionResponse = await fetch(userSubmissionsUrl, { + cache: "no-cache", + }); + const userSubmissionData = + (await userSubmissionResponse.json()) as RawUserSubmission; + + // change userSubmissionData.sub[] into UserSub[] + // originally the upstream api would return `userSubmissionData.sub` as an array of array + // // ex: + // [ + // [ + // 0: 5251911 // submission ID + // 1: 62 // problem ID + // 2: 10 // verdict ID + // 3: 0 // runtime + // 4: 1168359789 // submission time + // 5: 4 // language ID + // 6: -1 // rank + // ] + // ] + const converted = userSubmissionData.subs + .sort((a, b) => b[4] - a[4]) // sort by submission time in descending order + .slice(0, 500) // take only the most recent 500 submissions + .map((submission: number[]) => { + const converted: Partial = {}; + converted.sid = submission[0]; + converted.pid = submission[1]; + converted.ver = submission[2]; + converted.run = submission[3]; + converted.sbt = submission[4]; + converted.lan = submission[5] as unknown as string; + converted.rank = submission[6]; + + const problemData = allProblemsData.find( + (problem) => problem[0] === converted.pid, + ); + + converted.pnum = (problemData as RawProblem)[1]; + converted.pTitle = (problemData as RawProblem)[2]; + converted.verdict = ProblemVerdictMap[converted.ver as number] || { + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgColor: "bg-gray-500", + title: "- In Queue -", + fgHex: "", + bgHex: "6b7280", + }; + converted.lan = Language[converted.lan]; + + return converted; + }); + + const resultData: UserSubmission = { + name: userSubmissionData.name, + uname: userSubmissionData.uname, + subs: converted as UserSub[], + }; + + return Response.json(resultData); +}; diff --git a/src/app/api/users/[username]/submissions/verdict/route.ts b/src/app/api/users/[username]/submissions/verdict/route.ts new file mode 100644 index 0000000..f19e7c6 --- /dev/null +++ b/src/app/api/users/[username]/submissions/verdict/route.ts @@ -0,0 +1,89 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { userSchema as schema } from "@/schema"; +import { + uhuntUserSubmissionsUrl, + uhuntUsername2UidUrl, +} from "@/utils/constants"; +import { + ProblemVerdictMap, + UserSubmission, + UserSubmissionBarChartType, +} from "@/types"; + +type getParamsType = { + params: z.infer; +}; + +/** + * Get the user submissions by verdicts + * The data returned will be used in Rechart `bar` chart + */ +export const GET = async (_request: Request, { params }: getParamsType) => { + // validate params + const schemaResponse = await schema.safeParseAsync(params); + if (!schemaResponse.success) { + const message = { + message: schemaResponse.error.issues[0].message, + }; + + return NextResponse.json(message, { + status: 400, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch username ID + const { username } = params; + + const usernameUrl = uhuntUsername2UidUrl(username); + const usernameResponse = await fetch(usernameUrl); + const usernameData: number = await usernameResponse.json(); + + // return 404 if problem doesn't exist + if (usernameData === 0) { + const message = { + message: `Username ${username} not found`, + }; + return NextResponse.json(message, { + status: 404, + }); + } + + //----------------------------------------------------------------------------------------------// + + // fetch submissions of the user + const userSubmissionsUrl = uhuntUserSubmissionsUrl(usernameData); + const userSubmissionResponse = await fetch(userSubmissionsUrl, { + cache: "no-cache", + }); + const userSubmissionData = + (await userSubmissionResponse.json()) as UserSubmission; + + const filter = ["ac", "pe", "wa", "tle", "mle", "ce", "re", "ole"]; + // count the verdicts by their verdict ID. + // use verdictShort as the object key + // this will be used when later when constructing the data for recharts + const reducedData = (userSubmissionData.subs as unknown as number[][]).reduce( + (obj: Record, cur, _index) => { + const verdict = ProblemVerdictMap[cur[2]]; + obj[verdict.verdictShort] = obj[verdict.verdictShort] + 1 || 1; + + return obj; + }, + {}, + ); + + const processedData: UserSubmissionBarChartType[] = filter.map((verdict) => { + return { + name: verdict.toUpperCase(), + verdict: reducedData[verdict] || 0, + tooltipTitle: ProblemVerdictMap[verdict].title, + fill: ProblemVerdictMap[verdict].bgHex, + }; + }); + + return Response.json(processedData); +}; diff --git a/src/app/components/data-table/columns.tsx b/src/app/components/data-table/columns.tsx new file mode 100644 index 0000000..7f671b3 --- /dev/null +++ b/src/app/components/data-table/columns.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import moment from "moment"; +import Link from "next/link"; + +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"; +import { Submission } from "@/types"; +import { cn } from "@/lib/utils"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "submissionId", + accessorFn: (row) => row.msg.sid, + meta: { + // for displaying the columns dropdown + headerTitle: "Submission ID", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("submissionId")} + + ); + }, + }, + { + accessorKey: "problemNum", + accessorFn: (row) => row.msg.pnum, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Number", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemNum")} + + ); + }, + }, + { + accessorKey: "problemTitle", + accessorFn: (row) => row.msg.pTitle, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Title", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemTitle")} + + ); + }, + }, + { + accessorKey: "username", + accessorFn: (row) => `${row.msg.name} (${row.msg.uname})`, + meta: { + // for displaying the columns dropdown + headerTitle: "User (username)", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("username")} + + ); + }, + }, + { + accessorKey: "verdict", + accessorFn: (row) => row.msg.verdict.title, + meta: { + // for displaying the columns dropdown + headerTitle: "Verdict", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("verdict")} + + ); + }, + }, + { + accessorKey: "language", + accessorFn: (row) => row.msg.lan, + meta: { + // for displaying the columns dropdown + headerTitle: "Language", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("language")}

; + }, + enableSorting: false, + }, + { + accessorKey: "runtime", + accessorFn: (row) => row.msg.run, + meta: { + // for displaying the columns dropdown + headerTitle: "Runtime", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{((row.getValue("runtime") as number) / 1000).toFixed(3)}

; + }, + enableSorting: false, + }, + { + accessorKey: "rank", + accessorFn: (row) => row.msg.rank, + meta: { + // for displaying the columns dropdown + headerTitle: "Rank", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("rank")}

; + }, + enableSorting: false, + }, + { + accessorKey: "submitTime", + accessorFn: (row) => row.msg.sbt, + meta: { + // for displaying the columns dropdown + headerTitle: "Submit Time", + }, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + + +

+ {moment.unix(row.getValue("submitTime")).fromNow()} +

+
+ +

+ Submitted at{" "} + {moment.unix(row.getValue("submitTime")).toLocaleString()} +

+
+
+
+ ); + }, + enableSorting: false, + }, +]; diff --git a/src/app/globals.css b/src/app/globals.css index 0b46ea1..319770a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -73,4 +73,12 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + +.nav-link { + @apply rounded-lg p-2 hover:underline hover:dark:bg-zinc-800 hover:bg-zinc-200 +} + +.active-nav-link { + @apply rounded-lg p-2 underline dark:bg-zinc-800 bg-zinc-200 text-primary +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d498d66..91ac519 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Inter as FontSans } from "next/font/google"; import { ThemeProvider } from "@/components/theme-provider"; +import Navbar from "@/components/navbar"; +import ReactQueryProvider from "@/provider/ReactQuery"; import "./globals.css"; import { cn } from "@/lib/utils"; @@ -29,14 +31,17 @@ export default function RootLayout({ inter.variable, )} > - - {children} - + + + +
{children}
+
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index e50120b..3275e2a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,116 +1,43 @@ -import Image from 'next/image' +"use client"; -import {ModeToggle} from "@/components/darkmode-toggle" +import { useRef } from "react"; -export default function Home() { - return ( -
-
-

- Get started by editing  - src/app/page.tsx -

- -
+import { useFetchLiveSubmission } from "@/hooks"; +import { DataTable } from "@/components/ui/data-table"; +import DataTableLoading from "@/components/ui/data-table/loading"; +import { columns } from "@/app/components/data-table/columns"; -
- Next.js Logo -
+/////////////////////////////////////////////////////////////////////////////////////////////////// -
- -

- Docs{' '} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
+export default function Home() { + // used for setting the pollId. + // not using useState, because that will force a re-render every time it's set + // then it will fetch the new data, which will set the state, which will render the page + // which will refetch the from the server. Causing an infinite loop + let pollIdRef = useRef(0); + const { data, isLoading, isError, isSuccess } = useFetchLiveSubmission(pollIdRef.current); - -

- Learn{' '} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
+ if (isLoading || !data) { + return ( +
+

Live Submissions

+ +
+ ); + } - -

- Templates{' '} - - -> - -

-

- Explore the Next.js 13 playground. -

-
+ if (isError) { + return
Error fetching data
; + } - -

- Deploy{' '} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
- -
-
- ) + if (isSuccess) { + pollIdRef.current = data[0].msg.sid; + } + + return ( +
+

Live Submissions

+ +
+ ); } diff --git a/src/app/problems/[problemNum]/components/data-table/ranklistColumns.tsx b/src/app/problems/[problemNum]/components/data-table/ranklistColumns.tsx new file mode 100644 index 0000000..75142ed --- /dev/null +++ b/src/app/problems/[problemNum]/components/data-table/ranklistColumns.tsx @@ -0,0 +1,218 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import moment from "moment"; +import Link from "next/link"; + +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"; +import { Submission } from "@/types"; +import { cn } from "@/lib/utils"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "submissionId", + accessorFn: (row) => row.sid, + meta: { + // for displaying the columns dropdown + headerTitle: "Submission ID", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("submissionId")} + + ); + }, + }, + { + accessorKey: "problemNum", + accessorFn: (row) => row.pnum, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Number", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemNum")} + + ); + }, + enableSorting: false, + }, + { + accessorKey: "problemTitle", + accessorFn: (row) => row.pTitle, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Title", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemTitle")} + + ); + }, + enableSorting: false, + }, + { + accessorKey: "username", + accessorFn: (row) => `${row.name} (${row.uname})`, + meta: { + // for displaying the columns dropdown + headerTitle: "User (username)", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + if (row.original.uname === "--- ? ---") { + return ( +

{row.original.uname}

+ ) + } + return ( + + {row.getValue("username")} + + ); + }, + }, + { + accessorKey: "verdict", + accessorFn: (row) => row.verdict.title, + meta: { + // for displaying the columns dropdown + headerTitle: "Verdict", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("verdict")} + + ); + }, + enableSorting: false, + }, + { + accessorKey: "language", + accessorFn: (row) => row.lan, + meta: { + // for displaying the columns dropdown + headerTitle: "Language", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("language")}

; + }, + enableSorting: false, + }, + { + accessorKey: "runtime", + accessorFn: (row) => row.run, + meta: { + // for displaying the columns dropdown + headerTitle: "Runtime", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{((row.getValue("runtime") as number) / 1000).toFixed(3)}

; + }, + enableSorting: false, + }, + { + accessorKey: "rank", + accessorFn: (row) => row.rank, + meta: { + // for displaying the columns dropdown + headerTitle: "Rank", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("rank")}

; + }, + enableSorting: false, + }, + { + accessorKey: "submitTime", + accessorFn: (row) => row.sbt, + meta: { + // for displaying the columns dropdown + headerTitle: "Submit Time", + }, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + + +

+ {moment.unix(row.getValue("submitTime")).fromNow()} +

+
+ +

+ Submitted at{" "} + {moment.unix(row.getValue("submitTime")).toLocaleString()} +

+
+
+
+ ); + }, + enableSorting: false, + }, +]; diff --git a/src/app/problems/[problemNum]/loading.tsx b/src/app/problems/[problemNum]/loading.tsx new file mode 100644 index 0000000..ae01a6d --- /dev/null +++ b/src/app/problems/[problemNum]/loading.tsx @@ -0,0 +1,28 @@ +import Loading from '@/components/ui/data-table/loading' +import { Skeleton } from '@/components/ui/skeleton' +import React from 'react' + +const ProblemNumLoading = () => { + return ( +
+ +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+ ) +} + +export default ProblemNumLoading diff --git a/src/app/problems/[problemNum]/page.tsx b/src/app/problems/[problemNum]/page.tsx new file mode 100644 index 0000000..60728d5 --- /dev/null +++ b/src/app/problems/[problemNum]/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { AxiosError } from "axios"; +import { z } from "zod"; + +import Error from "@/components/error"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + useFetchProblemNum, + useFetchProblemRanklist, + useFetchProblemSubmission, + useFetchSubmissionCount, + useFetchSubmissionLang, +} from "@/hooks"; +import { problemNumSchema } from "@/schema"; +import { processProblemNumBarChartData } from "@/utils/dataProcessing"; +import ProblemVerdictChart from "@/components/charts/ProblemVerdictChart"; +import SubmissionsOvertimeChart from "@/components/charts/SubmissionsOvertimeChart"; +import SubmissionLanguageRadarChart from "@/components/charts/SubmissionLanguageRadarChart"; +import { DataTable } from "@/components/ui/data-table"; +import { columns } from "./components/data-table/ranklistColumns"; +import Loading from "./loading"; +import Link from "next/link"; +import { uhuntViewProblemUrl } from "@/utils/constants"; +import { Problem, Submission, SubmissionLangType } from "@/types"; + +type problemPageProps = { + params: z.infer; +}; + +const ProblemPage = ({ params }: problemPageProps) => { + const { + isLoading: problemNumIsLoading, + isSuccess: problemNumIsSuccess, + isError: problemNumIsError, + data: problemNumData, + error: problemNumError, + } = useFetchProblemNum(params.problemNum); + const { + isLoading: submissionCountIsLoading, + isSuccess: submissionCountIsSuccess, + isError: submissionCountIsError, + data: submissionCountData, + error: submissionCountError, + } = useFetchSubmissionCount(params.problemNum); + const { + isLoading: submissionLangIsLoading, + isSuccess: submissionLangIsSuccess, + isError: submissionLangIsError, + data: submissionLangData, + error: submissionLangError, + } = useFetchSubmissionLang(params.problemNum); + const { + isLoading: problemRanklistIsLoading, + isSuccess: problemRanklistIsSuccess, + isError: problemRanklistIsError, + data: problemRanklistData, + error: problemRanklistError, + } = useFetchProblemRanklist(params.problemNum); + const { + isLoading: problemSubmissionIsLoading, + isSuccess: problemSubmissionIsSuccess, + isError: problemSubmissionIsError, + data: problemSubmissionData, + error: problemSubmissionError, + } = useFetchProblemSubmission(params.problemNum); + + if ( + problemNumIsLoading || + submissionCountIsLoading || + submissionLangIsLoading || + problemRanklistIsLoading || + problemSubmissionIsLoading + ) { + return ; + } + + if (problemNumIsError) { + type ErrorMessage = { + message: string; + }; + + if ( + (problemNumError as AxiosError).response?.status === 400 + ) { + return ( + ).response?.data.message + } + /> + ); + } else if ( + (problemNumError as AxiosError).response?.status === 404 + ) { + return ; + } + } + + if (problemNumIsSuccess) { + // console.log(problemNumData); + } + + const processedProblemVerdictData = processProblemNumBarChartData( problemNumData as Problem); + return ( +
+ + {params.problemNum}: {( problemNumData as Problem ).title} + +
+ {/* Submission verdicts bar chart */} +
+ + + Submission Verdicts + + + + + +
+ {/* Submissions overtime line chart */} +
+ + + Submissions overtime + + + + + +
+
+ + + Submissions by language + + + + + +
+
+
+
+

Ranklist (Top 10)

+ +
+
+

Submissions

+ +
+
+
+ ); +}; + +export default ProblemPage; diff --git a/src/app/problems/components/data-table/columns.tsx b/src/app/problems/components/data-table/columns.tsx new file mode 100644 index 0000000..e07f2a3 --- /dev/null +++ b/src/app/problems/components/data-table/columns.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import Link from "next/link"; + +import { Button, buttonVariants } from "@/components/ui/button"; +import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"; +import { Problem } from "@/types"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "num", + meta: { + headerTitle: "Problem number" + }, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + {row.getValue("num")} + + ); + }, + }, + { + accessorKey: "title", + meta: { + headerTitle: "Problem title" + }, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + {row.getValue("title")} + + ); + }, + }, +]; diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx new file mode 100644 index 0000000..30764b2 --- /dev/null +++ b/src/app/problems/page.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { DataTable } from "@/components/ui/data-table"; +import DataTableLoading from "@/components/ui/data-table/loading"; +import { columns } from "@/app/problems/components/data-table/columns"; +import { useFetchProblems } from "@/hooks"; +import { Problem } from "@/types"; + +const ProblemsPage = () => { + const { data, isLoading, isError } = useFetchProblems(); + + if (isLoading) { + return ( +
+

All Problems

+ +
+ ); + } + + if (isError) { + return
Error fetching data
; + } + + return ( +
+

All Problems

+ +
+ ); +}; + +export default ProblemsPage; diff --git a/src/app/users/[username]/components/data-table/submissionColumns.tsx b/src/app/users/[username]/components/data-table/submissionColumns.tsx new file mode 100644 index 0000000..33a52a2 --- /dev/null +++ b/src/app/users/[username]/components/data-table/submissionColumns.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import moment from "moment"; +import Link from "next/link"; + +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"; +import { Submission, UserSub } from "@/types"; +import { cn } from "@/lib/utils"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "submissionId", + accessorFn: (row) => row.sid, + meta: { + // for displaying the columns dropdown + headerTitle: "Submission ID", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("submissionId")} + + ); + }, + }, + { + accessorKey: "problemNum", + accessorFn: (row) => row.pnum, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Number", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemNum")} + + ); + }, + enableSorting: false, + }, + { + accessorKey: "problemTitle", + accessorFn: (row) => row.pTitle, + meta: { + // for displaying the columns dropdown + headerTitle: "Problem Title", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("problemTitle")} + + ); + }, + enableSorting: false, + }, + // { + // accessorKey: "username", + // accessorFn: (row) => `${row.name} (${row.uname})`, + // meta: { + // // for displaying the columns dropdown + // headerTitle: "User (username)", + // }, + // header: ({ column }) => { + // return ; + // }, + // cell: ({ row }) => { + // if (row.original.uname === "--- ? ---") { + // return ( + //

{row.original.uname}

+ // ) + // } + // return ( + // + // {row.getValue("username")} + // + // ); + // }, + // }, + { + accessorKey: "verdict", + accessorFn: (row) => row.verdict.title, + meta: { + // for displaying the columns dropdown + headerTitle: "Verdict", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return ( + + {row.getValue("verdict")} + + ); + }, + enableSorting: false, + }, + { + accessorKey: "language", + accessorFn: (row) => row.lan, + meta: { + // for displaying the columns dropdown + headerTitle: "Language", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("language")}

; + }, + enableSorting: false, + }, + { + accessorKey: "runtime", + accessorFn: (row) => row.run, + meta: { + // for displaying the columns dropdown + headerTitle: "Runtime", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{((row.getValue("runtime") as number) / 1000).toFixed(3)}

; + }, + enableSorting: false, + }, + { + accessorKey: "rank", + accessorFn: (row) => row.rank, + meta: { + // for displaying the columns dropdown + headerTitle: "Rank", + }, + header: ({ column }) => { + return ; + }, + cell: ({ row }) => { + return

{row.getValue("rank")}

; + }, + enableSorting: false, + }, + { + accessorKey: "submitTime", + accessorFn: (row) => row.sbt, + meta: { + // for displaying the columns dropdown + headerTitle: "Submit Time", + }, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + + +

+ {moment.unix(row.getValue("submitTime")).fromNow()} +

+
+ +

+ Submitted at{" "} + {moment.unix(row.getValue("submitTime")).toLocaleString()} +

+
+
+
+ ); + }, + enableSorting: false, + }, +]; + diff --git a/src/app/users/[username]/loading.tsx b/src/app/users/[username]/loading.tsx new file mode 100644 index 0000000..53147ae --- /dev/null +++ b/src/app/users/[username]/loading.tsx @@ -0,0 +1,27 @@ +import Loading from "@/components/ui/data-table/loading"; +import { Skeleton } from "@/components/ui/skeleton"; +import React from "react"; + +const UsernameLoading = () => { + return ( +
+ +
+ {[...Array(4)].map((cur, index) => ( +
+ {" "} + {" "} +
+ ))} +
+
+
+ + +
+
+
+ ); +}; + +export default UsernameLoading; diff --git a/src/app/users/[username]/page.tsx b/src/app/users/[username]/page.tsx new file mode 100644 index 0000000..f4e04ff --- /dev/null +++ b/src/app/users/[username]/page.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { AxiosError } from "axios"; +import { z } from "zod"; + +import Error from "@/components/error"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { userSchema } from "@/schema"; +import SubmissionVerdictChart from "@/components/charts/ProblemVerdictChart"; +import { + useFetchUserSubmissionAttempted, + useFetchUserSubmissionLanguage, + useFetchUserSubmissionOvertime, + useFetchUserSubmissions, + useFetchUserSubmissionVerdict, +} from "@/hooks"; +import { + SubmissionLangType, + SubmissionSovledVsAttempted, + SubmissionsOvertimeLineChartType, + UserSubmission, + UserSubmissionBarChartType, +} from "@/types"; +import { DataTable } from "@/components/ui/data-table"; +import { columns } from "@/app/users/[username]/components/data-table/submissionColumns"; +import SubmissionLanguageRadarChart from "@/components/charts/SubmissionLanguageRadarChart"; +import SubmissionsOvertimeChart from "@/components/charts/SubmissionsOvertimeChart"; +import SolvedVsAttemptedDonutChart from "@/components/charts/SolvedVsAttemptedDonutChart"; +import Loading from "./loading"; + +type Props = { + params: z.infer; +}; + +const UserPage = ({ params }: Props) => { + const { + isFetching: userSubmissionIsFetching, + isError: userSubmissionIsError, + data: userSubmissionData, + error: userSubmissionError, + } = useFetchUserSubmissions(params.username); + const { + isFetching: userSubmissionVerdictIsFetching, + data: userSubmissionVerdictData, + } = useFetchUserSubmissionVerdict(params.username); + const { + isFetching: userSubmissionLanguageIsFetching, + data: userSubmissionLanguageData, + } = useFetchUserSubmissionLanguage(params.username); + const { + isFetching: userSubmissionOvertimeIsFetching, + data: userSubmissionOvertimeData, + } = useFetchUserSubmissionOvertime(params.username); + const { + isFetching: userSubmissionAttemptedIsFetching, + data: userSubmissionAttemptedData, + } = useFetchUserSubmissionAttempted(params.username); + + if ( + userSubmissionIsFetching || + userSubmissionVerdictIsFetching || + userSubmissionLanguageIsFetching || + userSubmissionOvertimeIsFetching || + userSubmissionAttemptedIsFetching + ) { + return ; + } + + if (userSubmissionIsError) { + type ErrorMessage = { + message: string; + }; + + const status = (userSubmissionError as AxiosError).response?.status; + const message = (userSubmissionError as AxiosError).response?.data.message; + + return ( + + ); + } + + return ( +
+
+

+ {userSubmissionData?.name} ({params.username}) +

+
+
+
+ + + Submission Verdicts + + + + + +
+ {/* Submissions overtime line chart */} +
+ + + Submissions overtime + + + + + +
+ {/* Submissions language using radar chart */} +
+ + + Submissions by language + + + + + +
+ {/* Problem solved vs attempted with donut chart */} +
+ + + Solved problems vs Problem submissions + + + + + +
+
+
+
+
+

Submissions

+ +
+
+
+
+ ); +}; + +export default UserPage; diff --git a/src/components/charts/ProblemVerdictChart.tsx b/src/components/charts/ProblemVerdictChart.tsx new file mode 100644 index 0000000..473ba04 --- /dev/null +++ b/src/components/charts/ProblemVerdictChart.tsx @@ -0,0 +1,48 @@ +/* + * To display a problem number submission verdicts in a bar chart + * + * Uses Rechart bar chart + */ + +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, +} from "recharts"; + +import { VerdictBarChartType } from "@/types"; +import ChartTooltip from "@/components/charts/Tooltip"; + +type Props = { + data: VerdictBarChartType[]; +}; + +const ProblemVerdictChart = ({ data }: Props) => { + return ( + + + + + + `${new Intl.NumberFormat("us").format(number).toString()}`} + labelFormatter={(payload) => payload[0].payload.tooltipTitle} + /> + } + /> + + + + ); +}; + +export default ProblemVerdictChart; diff --git a/src/components/charts/SolvedVsAttemptedDonutChart.tsx b/src/components/charts/SolvedVsAttemptedDonutChart.tsx new file mode 100644 index 0000000..f383d55 --- /dev/null +++ b/src/components/charts/SolvedVsAttemptedDonutChart.tsx @@ -0,0 +1,49 @@ +import { Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; + +import { SubmissionSovledVsAttempted } from "@/types"; +import { cn } from "@/lib/utils"; +import ChartTooltip from "@/components/charts/Tooltip"; + +type Props = { + data: SubmissionSovledVsAttempted[]; +}; + +const SolvedVsAttemptedDonutChart = ({ data }: Props) => { + return ( + + + + ( + `${new Intl.NumberFormat("us").format(number).toString()}`} + labelFormatter={(payload) => payload[0].payload.tooltipTitle} + /> + )} + /> + + + ); +}; + +export default SolvedVsAttemptedDonutChart; diff --git a/src/components/charts/SubmissionLanguageRadarChart.tsx b/src/components/charts/SubmissionLanguageRadarChart.tsx new file mode 100644 index 0000000..8751331 --- /dev/null +++ b/src/components/charts/SubmissionLanguageRadarChart.tsx @@ -0,0 +1,61 @@ +import { + Radar, + RadarChart, + PolarAngleAxis, + PolarGrid, + ResponsiveContainer, + Tooltip, +} from "recharts"; +import { useTheme } from "next-themes"; + +import ChartTooltip from "@/components/charts/Tooltip"; +import { SubmissionLangType } from "@/types"; + +type Props = { + data: SubmissionLangType[]; +}; + +const SubmissionLanguageRadarChart = ({ data }: Props) => { + const { theme } = useTheme(); + + return ( + + + + + ( + + `${new Intl.NumberFormat("us").format(number).toString()}` + } + // labelFormatter={(payload) => payload[0].payload.tooltipTitle} + /> + )} + /> + + + + {/* */} + + + + + + + ); +}; + +export default SubmissionLanguageRadarChart; diff --git a/src/components/charts/SubmissionsOvertimeChart.tsx b/src/components/charts/SubmissionsOvertimeChart.tsx new file mode 100644 index 0000000..e6abb7f --- /dev/null +++ b/src/components/charts/SubmissionsOvertimeChart.tsx @@ -0,0 +1,58 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, +} from "recharts"; + +import ChartTooltip from "@/components/charts/Tooltip"; +import { SubmissionsOvertimeLineChartType } from "@/types"; + +type Props = { + data: SubmissionsOvertimeLineChartType[]; +}; + +const SubmissionsOvertimeChart = ({ data }: Props) => { + return ( + + + + + ( + + `${new Intl.NumberFormat("us").format(number).toString()}` + } + // labelFormatter={(payload) => payload[0].payload.tooltipTitle} + /> + )} + /> + {/* linear gradient */} + {/* obtained from https://www.youtube.com/watch?v=e4en8kRqwe8 */} + + + + + + + + + + ); +}; + +export default SubmissionsOvertimeChart; diff --git a/src/components/charts/Tooltip.tsx b/src/components/charts/Tooltip.tsx new file mode 100644 index 0000000..66f5d29 --- /dev/null +++ b/src/components/charts/Tooltip.tsx @@ -0,0 +1,156 @@ +/* + * Custom Recharts tooltip component. + * Used in Rechart charts +* +* Code obtained from `tremor.so` +* https://github.com/tremorlabs/tremor/blob/main/src/components/chart-elements/common/ChartTooltip.tsx +*/ + +import { cn } from "@/lib/utils"; + +export const ChartTooltipFrame = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+); + +export interface ChartTooltipRowProps { + value: string; + name: string; + color: string; +} + +export const ChartTooltipRow = ({ value, name, color }: ChartTooltipRowProps) => { + return ( +
+
+ +

+ {name} +

+
+

+ {value} +

+
+) }; + +export interface ChartTooltipProps { + active: boolean | undefined; + payload: any; + label: string; + // categoryColors: Map; + valueFormatter: { + (value: number): string; + } + labelFormatter?: { + (payload:any ) : string; + } +} + +const ChartTooltip = ({ + active, + payload, + label, + // categoryColors, + valueFormatter, + labelFormatter +}: ChartTooltipProps) => { + if (active && payload) { + const filteredPayload: any[] = payload.filter((item: any) => item.type !== "none"); + + return ( + +
+

+ {labelFormatter ? labelFormatter(payload) : label} +

+
+ +
+ {filteredPayload.map(({ value, name, payload }: { value: number; name: string, payload: any }, idx: number) => ( + + ))} +
+
+ ); + } + return null; +}; + +export default ChartTooltip; diff --git a/src/components/error.tsx b/src/components/error.tsx new file mode 100644 index 0000000..e0e66fc --- /dev/null +++ b/src/components/error.tsx @@ -0,0 +1,18 @@ +type Props = { + status: number; + message?: string; +} + +const Error = ({status, message}: Props) => { + return ( +
+

{status}

+ {message && ( +

{message}

+ )} +
+ ) +} + +export default Error + diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx new file mode 100644 index 0000000..7360cde --- /dev/null +++ b/src/components/navbar.tsx @@ -0,0 +1,49 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { cn } from "@/lib/utils"; +import { ModeToggle } from "@/components/darkmode-toggle"; +import SearchBar from "./searchbar"; + +const links = [ + { label: "Uva uHunt", href: "/" }, + { label: "All problems", href: "/problems" }, +]; + +const Navbar = ({ className, ...props }: React.HTMLAttributes) => { + const pathname = usePathname(); + + return ( +
+ +
+ ); +}; + +export default Navbar; diff --git a/src/components/searchbar.tsx b/src/components/searchbar.tsx new file mode 100644 index 0000000..4972a09 --- /dev/null +++ b/src/components/searchbar.tsx @@ -0,0 +1,61 @@ +"use client"; + +import Link from "next/link"; +import { Search } from "lucide-react"; +import { useState } from "react"; + +import { Card, CardDescription, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { useFetchSearch } from "@/hooks"; +import { cn } from "@/lib/utils"; + +const placeholderText = ["Search problem number", "Search username"]; + +const SearchBar = () => { + const [searchStr, setSearchStr] = useState(""); + const { data: searchData } = useFetchSearch(searchStr); + + return ( +
+
+
+ + setSearchStr(event.target.value)} + value={searchStr} + className="pl-8" + placeholder={placeholderText[Math.floor(Math.random() * 2)]} + /> +
+
+
+ setSearchStr("")} + className={cn( + "flex flex-col divide-y", + searchData && "ring-2 ring-ring", + )} + > + {searchData && + searchData.length > 0 && + searchData.map((cur, index) => ( + + + + {cur.title} + + + + ))} + +
+
+ ); +}; + +export default SearchBar; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/data-table/column-header.tsx b/src/components/ui/data-table/column-header.tsx new file mode 100644 index 0000000..ff4f4b5 --- /dev/null +++ b/src/components/ui/data-table/column-header.tsx @@ -0,0 +1,78 @@ +import { + ArrowDownIcon, + ArrowUpIcon, + CaretSortIcon, + EyeNoneIcon, +} from "@radix-ui/react-icons" +import { SeparatorHorizontal } from "lucide-react"; +import { Column } from "@tanstack/react-table" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Ascending + + column.toggleSorting(true)}> + + Descending + + column.clearSorting()}> + + Default + + + column.toggleVisibility(false)}> + + Hide Column + + + +
+ ) +} diff --git a/src/components/ui/data-table/index.tsx b/src/components/ui/data-table/index.tsx new file mode 100644 index 0000000..8bae903 --- /dev/null +++ b/src/components/ui/data-table/index.tsx @@ -0,0 +1,183 @@ +"use client" + +import { useState } from "react" +import { Cross2Icon, MixerHorizontalIcon } from "@radix-ui/react-icons" +import { + ColumnDef, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { DataTablePagination } from "@/components/ui/data-table/pagination" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] + /** + * height of the table. If specified, it will overflow the table contents + */ + height?: number +} + +export function DataTable({ + columns, + data, + height +}: DataTableProps) { + const [sorting, setSorting] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [globalFilter, setGlobalFilter] = useState("") + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + onGlobalFilterChange: setGlobalFilter, + state: { + sorting, + columnVisibility, + globalFilter + }, + }) + + return ( +
+
+
+ setGlobalFilter(event.target.value)} + className="max-w-sm" + /> + {globalFilter.length > 0 && ( + + )} +
+ + + + + + Toggle columns + + {table + .getAllColumns() + .filter( + (column) => column.getCanHide() + ) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + { + /* + * to use column.columnDef.meta.headerTitle , it needs to be set in the `columns.tsx` + * export const columns: ColumnDef[] = [ + * meta: { + * headerTitle: "Problem number" + * }, + * ... // other config + * ] + */ + (column.columnDef.meta as { headerTitle: string })?.headerTitle || column.id + } + + ) + })} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ +
+ ) +} diff --git a/src/components/ui/data-table/loading.tsx b/src/components/ui/data-table/loading.tsx new file mode 100644 index 0000000..e4ee1da --- /dev/null +++ b/src/components/ui/data-table/loading.tsx @@ -0,0 +1,59 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Input } from "../input"; + +type Props = { + numColumns?: number; + numRows?: number; +}; + +const DataTableLoading = ({ numColumns = 5, numRows = 5 }: Props) => { + return ( +
+
+
+ +
+ +
+ + + + {[...Array(numColumns)].map((_, tableColsIndex) => ( + + + + ))} + + + + {[...Array(numRows)].map((_, i) => { + return ( + + {[...Array(numColumns)].map((_, k) => ( + + + + ))} + + ); + })} + +
+
+ +
+ +
+
+ ); +}; + +export default DataTableLoading; diff --git a/src/components/ui/data-table/pagination.tsx b/src/components/ui/data-table/pagination.tsx new file mode 100644 index 0000000..0833b20 --- /dev/null +++ b/src/components/ui/data-table/pagination.tsx @@ -0,0 +1,97 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from "@radix-ui/react-icons" +import { Table } from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +interface DataTablePaginationProps { + table: Table +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( +
+
+ {/* {table.getFilteredSelectedRowModel().rows.length} of{" "} */} + Total {table.getFilteredRowModel().rows.length} item(s). +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+
+ + + + +
+
+
+ ) +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..30fc44d --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/components/virtual-table/index.tsx b/src/components/virtual-table/index.tsx new file mode 100644 index 0000000..ddc788b --- /dev/null +++ b/src/components/virtual-table/index.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useRef } from "react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useVirtualizer } from "@tanstack/react-virtual"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; + +interface Props { + columns: ColumnDef[]; + data: TData[]; + /** + * Height of the table. Must be a number denoting pixels + * + * Ex: 400 would mean 400px. this will be set using tailwindcss `h-[400px]` + */ + tableHeight: number; +} + +/** + * A Virtual table using `@tanstack/react-table`. The virtualization is from `@tanstack/react-virtual` + * + * Code obtained from + * - ./src/components/ui/data-table/index.tsx + * - https://codesandbox.io/p/devbox/tanstack-table-example-virtualized-rows-33u7fj?file=%2Fsrc%2Fmain.tsx + * - https://codesandbox.io/p/devbox/tanstack-react-virtual-example-dynamic-mr8t3x?file=%2Fsrc%2Fmain.tsx + */ +function VirtualTable({ + columns, + data, + tableHeight, +}: Props) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + const { rows } = table.getRowModel(); + + //The virtualizer needs to know the scrollable container element + const tableContainerRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: rows.length, + estimateSize: () => 33, //estimate row height for accurate scrollbar dragging + getScrollElement: () => tableContainerRef.current, + //measure dynamic row height, except in firefox because it measures table border height incorrectly + measureElement: + typeof window !== "undefined" && + navigator.userAgent.indexOf("Firefox") === -1 + ? (element) => element?.getBoundingClientRect().height + : undefined, + overscan: 5, + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index]; + + return ( + rowVirtualizer.measureElement(node)} + key={row.id} + > + {row.getVisibleCells().map((cell) => { + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + ); + })} + +
+
+
+ ); +} + +export default VirtualTable; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..d6dc9f1 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,273 @@ +import axios from "axios"; +import { useQuery } from "@tanstack/react-query"; + +import { + Problem, + SearchResultType, + Submission, + SubmissionLangType, + SubmissionSovledVsAttempted, + SubmissionsOvertimeLineChartType, + UserSubmission, + UserSubmissionBarChartType, +} from "@/types"; + +/** + * Enum for React Query Keys when using React-query + */ +export enum queryKey { + polling = "live-polling", + /** + * Reacy query key for fetching all problems + */ + allProblems = "all-problems", + /** + * React query key for fetching a problem num + */ + problemNum = "problem-num", + /** + * React query key for fetching a problem ranklist + */ + problemRanklist = "problem-ranklist", + /** + * React query key for fetching a problem ranklist + */ + problemSubmission = "problem-submission", + /** + * React query key for fetching submission overtime count + */ + submissionCount = "submission-overtime", + /** + * React query key for fetching submission by language + */ + submissionLang = "submission-language", + //////////////////////////////////////////////////////////////////////////////////////////////// + /** + * React query key for fetching user submissions + */ + userSubmissions = "user-submissions", + /** + * React-query key for fetching user submissions verdict + */ + userSubmissionVerdict = "user-submissions-verdict", + /** + * React-query key for fetching user submissions language + */ + userSubmissionLanguage = "user-submissions-language", + /** + * React-query key for fetching user submissions overtime + */ + userSubmissionOvertime = "user-submissions-overtime", + /** + * React-query key for fetching user solved problems vs attempted submisssions + */ + userSubmissionAttempted = "user-submissions-attempted", + //////////////////////////////////////////////////////////////////////////////////////////////// + search = "search", +} + +/** + * Fetch live submission (polling) + * + * @param pollId - pollId to retrieve the live submission + * @param fetchInterval - time in milliseconds to refetch from the server + */ +export const useFetchLiveSubmission = (pollId = 0, fetchInterval = 5000) => { + return useQuery({ + queryKey: [queryKey.polling], + queryFn: async () => { + const { data } = await axios.get(`/api/poll/${pollId}`); + + return data; + }, + refetchInterval: fetchInterval, + staleTime: fetchInterval, + }); +}; + +/** + * Fetch all problems + */ +export const useFetchProblems = () => { + return useQuery({ + queryKey: [queryKey.allProblems], + queryFn: async () => + axios.get("/api/problems").then((res) => res.data), + refetchOnWindowFocus: false, + refetchOnMount: false, + }); +}; + +/** + * Fetch stats of a problem using problem number + */ +export const useFetchProblemNum = (problemNum: number) => { + return useQuery({ + queryKey: [queryKey.problemNum], + queryFn: async () => + await axios + .get(`/api/problems/${problemNum}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch submissions overtime count + */ +export const useFetchSubmissionCount = (problemNum: number) => { + return useQuery({ + queryKey: [queryKey.submissionCount], + queryFn: async () => + await axios + .get(`/api/submissions/overtime/${problemNum}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch submissions by language + */ +export const useFetchSubmissionLang = (problemNum: number) => { + return useQuery({ + queryKey: [queryKey.submissionLang], + queryFn: async () => + await axios + .get(`/api/submissions/language/${problemNum}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch problem ranklist + */ +export const useFetchProblemRanklist = (problemNum: number) => { + return useQuery({ + queryKey: [queryKey.problemRanklist], + queryFn: async () => + await axios + .get(`/api/problems/ranklist/${problemNum}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch problem submissions + */ +export const useFetchProblemSubmission = (problemNum: number) => { + return useQuery({ + queryKey: [queryKey.problemSubmission], + queryFn: async () => + await axios + .get(`/api/submissions/${problemNum}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Fetch user submissions + */ +export const useFetchUserSubmissions = (username: string) => { + return useQuery({ + queryKey: [queryKey.userSubmissions], + queryFn: async () => + await axios + .get(`/api/users/${username}/submissions`) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch user submissions verdicts + * Used for displaying data using Rechart bar chart + */ +export const useFetchUserSubmissionVerdict = (username: string) => { + return useQuery({ + queryKey: [queryKey.userSubmissionVerdict], + queryFn: async () => + await axios + .get( + `/api/users/${username}/submissions/verdict`, + ) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch user submissions by language + * Used for displaying data using Rechart radar chart + */ +export const useFetchUserSubmissionLanguage = (username: string) => { + return useQuery({ + queryKey: [queryKey.userSubmissionLanguage], + queryFn: async () => + await axios + .get( + `/api/users/${username}/submissions/language`, + ) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch user submissions by language + * Used for displaying data using Rechart radar chart + */ +export const useFetchUserSubmissionOvertime = (username: string) => { + return useQuery({ + queryKey: [queryKey.userSubmissionOvertime], + queryFn: async () => + await axios + .get( + `/api/users/${username}/submissions/overtime`, + ) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/** + * Fetch user solved problems vs attempted submissions + * Used for displaying data using Rechart donut chart + */ +export const useFetchUserSubmissionAttempted = (username: string) => { + return useQuery({ + queryKey: [queryKey.userSubmissionAttempted], + queryFn: async () => + await axios + .get( + `/api/users/${username}/submissions/attempted`, + ) + .then((res) => res.data), + refetchOnWindowFocus: false, + }); +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Fetch search results + */ +export const useFetchSearch = (searchStr: string) => { + return useQuery({ + queryKey: [queryKey.search, searchStr], + queryFn: async (context) => + await axios + .get(`/api/search/${context.queryKey[1]}`) + .then((res) => res.data), + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + enabled: !!searchStr, + retry: false, + }); +}; diff --git a/src/provider/ReactQuery.tsx b/src/provider/ReactQuery.tsx new file mode 100644 index 0000000..c7c808e --- /dev/null +++ b/src/provider/ReactQuery.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { PropsWithChildren, useState } from "react"; + +const ReactQueryProvider = ({ children }: PropsWithChildren) => { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + {children} + {process.env.NODE_ENV !== "production" && ( + + )} + + ); +}; + +export default ReactQueryProvider; diff --git a/src/schema/index.ts b/src/schema/index.ts new file mode 100644 index 0000000..9947ca5 --- /dev/null +++ b/src/schema/index.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +/** + * Schema validation for endpoint `/api/problems/[problemNum]` + */ +export const problemNumSchema = z.object({ + problemNum: z.coerce + .number({ invalid_type_error: "Problem number must be a number" }) + .min(1, "Problem number must be a number greater than 0"), +}); + +export const submissionOvertimeSchema = z.object({ + problemNum: z.coerce + .number({ invalid_type_error: "Problem number must be a number" }) + .min(1, "Problem number must be a number greater than 0"), +}) + +export const submissionLangSchema = z.object({ + problemNum: z.coerce + .number({ invalid_type_error: "Problem number must be a number" }) + .min(1, "Problem number must be a number greater than 0"), +}) + +export const problemNumRanklistSchema = z.object({ + problemNum: z.coerce + .number({ invalid_type_error: "Problem number must be a number" }) + .min(1, "Problem number must be a number greater than 0"), +}) + +export const problemNumSubmissionSchema = z.object({ + problemNum: z.coerce + .number({ invalid_type_error: "Problem number must be a number" }) + .min(1, "Problem number must be a number greater than 0"), +}) + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Schema validation for endpoint `/api/users/[username]/submissions` + */ +export const userSchema = z.object({ + username: z.coerce.string().min(1, "Username must have at least on character") +}) + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Schema validation for endpoint `/api/search[searchStr] + */ +export const searchSchema = z.object({ + searchStr: z.coerce.string().min(1, "Search text must have at least on character") +}) + diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..d55187b --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,572 @@ +/* + * Contains all types for the upstream api server (uva uhunt https://uhunt.onlinejudge.org/api ) + * + * This file also contains objects that are used to convert a number from any of these types + * into a string. Ex: (submission verdict ID converted to a string). + */ + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ProblemStatus: Record = { + 0: "Unavailable", + 1: "Normal", + 2: "Special judge", +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Problem verdict data structure + */ +export interface ProblemVerdictType { + /** + * Title to be displayed on the front end + */ + title: string; + /** + * Tailwindcss for background color + * Usually used for displaying verdict using shadcn-ui `Badge` component + */ + bgColor: string; + /** + * Tailwindcss for foreground color + * Usually used for displaying verdict using shadcn-ui `Badge` component + */ + fgColor: string; + /** + * Hexcode of property `bgColor` + * Usually used for displaying verdict using shadcn-ui `Badge` component + */ + bgHex: string; + /** + * Hexcode of property `fgColor` + * Usually used for displaying verdict using shadcn-ui `Badge` component + */ + fgHex: string; + /** + * Verdict shorthand string + */ + verdictShort: string; +} + +const ProblemVerdictMap: Record = { + /** + * Number of Accepted + */ + ac: { + title: "Accepted", + bgColor: "bg-[#00AA00]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#00AA00", + fgHex: "", + verdictShort: "ac" + }, + /** + * Number of Presentation Error + */ + pe: { + title: "Presentation error", + bgColor: "bg-[#666600]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#666600", + fgHex: "", + verdictShort: "pe" + }, + /** + * Number of Wrong Answer + */ + wa: { + title: "Wrong answer", + bgColor: "bg-[#FF0000]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#FF0000", + fgHex: "", + verdictShort: "wa" + }, + /** + * Number of Time Limit Exceeded + */ + tle: { + title: "Time limit exceeded", + bgColor: "bg-[#0000FF]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#0000FF", + fgHex: "", + verdictShort: "tle" + }, + /** + * Number of Memory Limit Exceeded + */ + mle: { + title: "Memory limit exceeded", + bgColor: "bg-[#0000AA]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#0000AA", + fgHex: "", + verdictShort: "mle" + }, + /** + * Number of Compilation Error + */ + ce: { + title: "Compile error", + bgColor: "bg-orange-600", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#EA5A0C", + fgHex: "", + verdictShort: "ce" + }, + /** + * Number of Runtime Error + */ + re: { + title: "Runtime error", + bgColor: "bg-[#00AAAA]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#00AAAA", + fgHex: "", + verdictShort: "re" + }, + /** + * Number of Output Limit Exceeded + */ + ole: { + title: "Output limit exceeded", + bgColor: "bg-[#000066]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#000066", + fgHex: "", + verdictShort: "ole" + }, + /** + * Number of Submission Error + */ + sube: { + title: "Submission Error", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#6b7280", + fgHex: "", + verdictShort: "sube" + }, + /** + * Number of Can't be Judged + */ + noj: { + title: "Can't be judged", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#6b7280", + fgHex: "", + verdictShort: "noj" + }, + /** + * Number of In Queue + */ + inq: { + title: "- In Queue -", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#6b7280", + fgHex: "", + verdictShort: "inq" + }, + /** + * Number of Restricted Function + */ + rf: { + title: "Restricted function", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#6b7280", + fgHex: "", + verdictShort: "rf" + } +} +// adding keys from `Submission.ver` into object `ProblemVerdictMap` +// will reduce the duplicate code +// these verdict IDs have the same styling properties +ProblemVerdictMap["0"] = {...ProblemVerdictMap.inq }; +ProblemVerdictMap["10"] = {...ProblemVerdictMap.sube }; +ProblemVerdictMap["15"] = {...ProblemVerdictMap.noj }; +ProblemVerdictMap["20"] = {...ProblemVerdictMap.inq }; +ProblemVerdictMap["30"] = {...ProblemVerdictMap.ce }; +ProblemVerdictMap["35"] = {...ProblemVerdictMap.rf }; +ProblemVerdictMap["40"] = {...ProblemVerdictMap.re }; +ProblemVerdictMap["45"] = {...ProblemVerdictMap.ole }; +ProblemVerdictMap["50"] = {...ProblemVerdictMap.tle }; +ProblemVerdictMap["60"] = {...ProblemVerdictMap.mle }; +ProblemVerdictMap["70"] = {...ProblemVerdictMap.wa }; +ProblemVerdictMap["80"] = {...ProblemVerdictMap.pe }; +ProblemVerdictMap["90"] = {...ProblemVerdictMap.ac }; +export { ProblemVerdictMap }; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Data structure returned when querying from url `https://uhunt.onlinejudge.org/api/p/num/:num` + * + * Example `https://uhunt.onlinejudge.org/api/p/num/1760` + */ +export type Problem = { + /** + * Problem ID + */ + pid: number; + /** + * Problem number + */ + num: number; + /** + * Problem title + */ + title: string; + /** + * Number of Distinct Accepted User (DACU) + */ + dacu: number; + /** + * Best Runtime of an Accepted Submission + */ + mrun: number; + /** + * Best Memory used of an Accepted Submission + */ + mmem: number; + /** + * Number of No Verdict Given (can be ignored) + */ + nover: number; + /** + * Number of Submission Error + */ + sube: number; + /** + * Number of Can't be Judged + */ + noj: number; + /** + * Number of In Queue + */ + inq: number; + /** + * Number of Compilation Error + */ + ce: number; + /** + * Number of Restricted Function + */ + rf: number; + /** + * Number of Runtime Error + */ + re: number; + /** + * Number of Output Limit Exceeded + */ + ole: number; + /** + * Number of Time Limit Exceeded + */ + tle: number; + /** + * Number of Memory Limit Exceeded + */ + mle: number; + /** + * Number of Wrong Answer + */ + wa: number; + /** + * Number of Presentation Error + */ + pe: number; + /** + * Number of Accepted + */ + ac: number; + /** + * Problem Run-Time Limit (milliseconds) + */ + rtl: number; + /** + * Problem Status (0 = unavailable, 1 = normal, 2 = special judge) + */ + status: string; + rej: number; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +export const Language: Record = { + 1: "ANSI C", + 2: "Java", + 3: "C++", + 4: "Pascal", + 5: "C++11", + 6: "Python", +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Submission data structure + * + * Example `https://uhunt.onlinejudge.org/api/poll/0` + */ +export type Submission = { + /** + * Poll ID + */ + id: number; + type: string; + msg: { + /** + * Submission ID + */ + sid: number; + /** + * User ID + */ + uid: number; + /** + * Problem ID + */ + pid: number; + /** + * Problem number + */ + pnum: number; + /** + * Problem title + */ + pTitle: string; + /** + * Verdict ID + */ + ver: number; + /** + * Verdict properties + * + * contains + * - title: verdict title Ex: `Accepted`, or `Compile error` + * - fgColor: tailwindcss class to use for foreground color + * - bgColor: tailwindcss class to use for background color + * - fgHex: hexcode of the property `fgColor` + * - bgHex: hexcode of property `bgColor` + */ + verdict: ProblemVerdictType; + /** + * Language ID + */ + lan: string; + /** + * Runtime in seconds + */ + run: number; + /** + * Memory used for the submission + */ + mem: number; + /** + * Submission rank of the problem + */ + rank: number | string; + /** + * Submission time in Unix timestamp + */ + sbt: number; + /** + * User's name for the submission + */ + name: string; + /** + * the username of the user + */ + uname: string; + }; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Submissions array returned when fetching user submissions + */ +export type UserSub = { + sid: number; + pid: number; + pnum: number; + pTitle: string; + ver: number; + verdict: ProblemVerdictType, + run: number; + sbt: number + lan: string; + rank: number +} + +// user submissions + +export type UserSubmission = { + /** + * name of the user + */ + name: string; + /** + * username of the user + */ + uname: string; + /** + * User submissions + */ + subs: UserSub[] +} + +/** + * Data structure for display user submissions using Rechart bar chart + * Used in api endpoint `/api/users/[username]/submissions/verdict` + */ +export type UserSubmissionBarChartType = { + /** + * Name of the bar in the bar chart. + * usually the verdict acronyms + */ + name: string; + /** + * The value of the verdict + */ + verdict: number; + /** + * Tooltip title to display. + * Usually would be the full string of a verdict + */ + tooltipTitle: string; + /** + * Color for bar + */ + fill: string; +}; + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Data type used by Rechart radar chart. + * + * This is used to display submissions by language + */ +export type SubmissionLangType = { + /** + * Programmign language used for the submission + */ + language: string; + /** + * The sum of submissions using this language + */ + count: number; +}; + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Data type used by Rechart bar chart. + * Specifically used for `Problem verdict bar chart` and `Submission verdict bar chart` + * + * This is used to display submissions by verdict + */ +export type VerdictBarChartType = { + /** + * Name of the bar in the bar chart. + * usually the verdict acronyms + */ + name: string; + /** + * The value of the verdict + */ + verdict: number; + /** + * Tooltip title to display. + * Usually would be the full string of a verdict + */ + tooltipTitle: string; + /** + * Color for bar + */ + fill: string; +}; + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Data type used by Rechart area chart. + * + * Specifically used for `Problem submissions overtime` and `User submissions overtime` + * + * This is used to display submissions overtime + */ +export type SubmissionsOvertimeLineChartType = { + /** + * name of the entry + */ + name: string; + /** + * Submission year. Denoted as YYYY + */ + time: string; // time formatted to year + /** + * Submission count for the year + */ + submissions: number; + /** + * Color used for the chart + */ + fill: string; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Data type used by Rechart dounut chart. + * + * This is used to display submissions solved vs attempted + */ +export type SubmissionSovledVsAttempted = { + /** + * Name of segment + */ + name: string; + /** + * Value of segment + */ + count: number; + /** + * Tooltip title to display. + * Usually would be the full string of a verdict + */ + tooltipTitle: string; + /** + * Color used for the chart + */ + fill: string; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Search result data structure returned when searching in the navbar + */ +export type SearchResultType = { + /** + * Name of the search result + * + * - Could be a problem number + * - Could be a user + */ + title: string; + /** + * href of the search result + * + * - If the result is a problem number, then navigate to `/problems/[problemNum]` + * - If the result is a user, then navigate to `/users/[username]` + */ + href: string; +} diff --git a/src/types/raw.ts b/src/types/raw.ts new file mode 100644 index 0000000..6d2611f --- /dev/null +++ b/src/types/raw.ts @@ -0,0 +1,185 @@ +/** + * Raw Problem data return from uhunt apo + * + * https://uhunt.onlinejudge.org/api/p/id/[problemNum] + * + * Ex: https://uhunt.onlinejudge.org/api/p/num/100 + * + * Also used in uhunt api. NOTE: it returns an array of `RawProblem` + * https://uhunt.onlinejudge.org/api/p + * + * contains the data in order + * - problem ID + * - problem number + * - problem title + * - Number of Distinct Accepted User (DACU) + * - Best Runtime of an Accepted Submission + * - Best Memory used of an Accepted Submission + * - Number of No Verdict Given (can be ignored) + * - Number of Submission Error + * - Number of Can't be Judged + * - Number of In Queue + * - Number of Compilation Error + * - Number of Restricted Function + * - Number of Runtime Error + * - Number of Output Limit Exceeded + * - Number of Time Limit Exceeded + * - Number of Memory Limit Exceeded + * - Number of Wrong Answer + * - Number of Presentation Error + * - Number of Accepted + * - Problem Run-Time Limit (milliseconds) + * - Problem Status (0 = unavailable, 1 = normal, 2 = special judge) + */ +export type RawProblem = [ + /** + * Problem ID + */ + number, + /** + * Problem number + */ + number, + /** + * Problem title + */ + string, + /** + * Number of Distinct Accepted User (DACU) + */ + number, + /** + * Best Runtime of an Accepted Submission + */ + number, + /** + * Best Memory used of an Accepted Submission + */ + number, + /** + * Number of No Verdict Given (can be ignored) + */ + number, + /** + * Number of Submission Error + */ + number, + /** + * Number of Can't be Judged + */ + number, + /** + * Number of In Queue + */ + number, + /** + * Number of Compilation Error + */ + number, + /** + * Number of Restricted Function + */ + number, + /** + * Number of Runtime Error + */ + number, + /** + * Number of Output Limit Exceeded + */ + number, + /** + * Number of Time Limit Exceeded + */ + number, + /** + * Number of Memory Limit Exceeded + */ + number, + /** + * Number of Wrong Answer + */ + number, + /** + * Number of Presentation Error + */ + number, + /** + * Number of Accepted + */ + number, + /** + * Problem Run-Time Limit (milliseconds) + */ + number, + /** + * Problem Status (0 = unavailable, 1 = normal, 2 = special judge) + */ + number, + number, +]; + +////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Raw User sub + * + * contains the data in order + * - Submission ID + * - Problem ID + * - Verdict ID + * - Runtime + * - Submission time (unix timestamp) + * - Language ID + * - Submission Rank + */ +export type RawUserSubs = [ + /** + * Submission ID + */ + number, + /** + * Problem ID + */ + number, + /** + * Verdict ID + */ + number, + /** + * Runtime + */ + number, + /** + * Submission time (unix timestamp) + */ + number, + /** + * Language ID + * - 1=ANSI C + * - 2=Java + * - 3=C++ + * - 4=Pascal + * - 5=C++11 + * - 6=Python + */ + number, + /** + * Submission rank + */ + number, +]; + +/** + * Raw data returned when fetching from uhunt api + * + * https://uhunt.onlinejudge.org/api/subs-user/[userID] + * + * Ex: https://uhunt.onlinejudge.org/api/subs-user/339 + */ +export type RawUserSubmission = { + name: string; + uname: string; + subs: RawUserSubs[]; +}; + +////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..b481ea5 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,104 @@ +import moment from "moment"; + +/** + * Base uHunt APU URL + */ +export const uhuntBaseApiUrl = "https://uhunt.onlinejudge.org/api"; + +/** + * URL for viewing the problem. + * Need to provide pid. NOT problem number + * Just append the pid. + */ +export const viewProblemUrl = + "https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=24&page=show_problem&problem="; + +/** + * URL for viewing the details of the problem PDF using problem ID + * @param {String | Number} pid problem ID + */ +export const uhuntViewProblemUrl = (pid: string | number) => `${viewProblemUrl}${pid}`; + +/** + * URL for fetching all problems on uva-uhunt + */ +export const uhuntAllProblemsUrl = () => 'https://uhunt.onlinejudge.org/api/p'; + +/** + * URL for getting Problem data using Problem ID + * @param {String | Number} pid - problem ID + */ +export const uhuntProblemIdUrl = (pid: string | number) => + `${uhuntBaseApiUrl}/p/id/${pid}`; + +/** + * URL for getting Problem data using Problem number + * @param {String | Number} problemNumber - problem number + */ +export const uhuntProblemNumUrl = (problemNumber: string | number) => + `${uhuntBaseApiUrl}/p/num/${problemNumber}`; + +/** + * URL for getting Problem ranklist using Problem ID, starting rank and number of ranks to return + * @param {String | Number} pid - Problem ID + * @param {Number} [start=1] - rank to start from. Default is 1 + * @param {Number} [count=100] - rank to go up to. Default is 100 + */ +export const uhuntProblemRankUrl = (pid: string | number, start = 1, count = 100) => + `${uhuntBaseApiUrl}/p/rank/${pid}/${start}/${count}`; + +/** + * URL for getting Submission count for a problem. + * By default it will return 20 years of data, + * represented in 20 array elements, each element represents 12 months. + * The submissionTime represents at what time should look back from, + * could be start from 5 years ago or now. + * @param {String | Number} pid - problem ID + * @param {Number} [submissionTime=moment().unix()] - Unix timestamp. Default is current unix timestamp + * @param {Number} [back=20] - Number of years to look back. Default is 20 + * @param {Number} [jump=12] - Number of months each array element will represent. Default is 12 + */ +export const uhuntSubmissionCountUrl = ( + pid: string | number, + submissionTime = moment().unix(), + back = 20, + jump = 12, +) => `${uhuntBaseApiUrl}/p/count/${pid}/${submissionTime}/${back}/${jump}`; + +/** + * URL for getting User ID using username + * @param {String} username - username + */ +export const uhuntUsername2UidUrl = (username: string) => + `${uhuntBaseApiUrl}/uname2uid/${username}`; + +/** + * URL for getting Submission list of a problem using Problem ID + * NOTE: this will return a large number of array elements. Recommend to return 1 year worth of submissions + * + * @param {String | Number} pid - problem ID + * @param {Number} [startSubmission=moment().subtract(1, 'years').unix()] - Unix timestamp for what time to start searching. Default is 1 year ago + * @param {Number} [endSubmission=moment().unix()] - Unix timestamp for what time to end searching. Default is right now + * @param {Number} [limit=500] - Number of submissions to return. Default is 500 submissions + */ +export const uhuntProblemSubmissionListUrl = ( + pid: string | number, + startSubmission = moment().subtract(1, 'years').unix(), + endSubmission = moment().unix(), + limit = 500 +) => `${uhuntBaseApiUrl}/p/subs/${pid}/${startSubmission}/${endSubmission}/${limit}`; + +/** + * Get User submissions using UserID + * @param {String | Number} uid User ID + */ +export const uhuntUserSubmissionsUrl = (uid: string | number) => + `${uhuntBaseApiUrl}/subs-user/${uid}`; + +/** + * URL for fetching all uhunt problems + */ +export const fetchAllProblemsUrl = () => `${uhuntBaseApiUrl}/p`; + +export const fetchLiveSubmissionsUrl = (pollId = 0) => + `${uhuntBaseApiUrl}/poll/${pollId}`; diff --git a/src/utils/dataProcessing.ts b/src/utils/dataProcessing.ts new file mode 100644 index 0000000..4013e38 --- /dev/null +++ b/src/utils/dataProcessing.ts @@ -0,0 +1,46 @@ +import { Language, Problem, ProblemVerdictMap, ProblemVerdictType } from "@/types"; +import {getResponseType as submissionLangType} from '@/app/api/submissions/language/[problemNum]/route' +import { SubmissionLangType } from "@/types"; +import { VerdictBarChartType } from "@/types"; + +export const processProblemNumBarChartData = (data: Problem) => { + // filter out the ProblemVerdictMap object and keep keys from `filter` array + const filter = ["ac", "pe", "wa", "tle", "mle", "ce", "re", "ole"] + // const filteredVerdicts: Record = {} + // filter.forEach(( key: string ) => { + // filteredVerdicts[key] = ProblemVerdictMap[key] + // }) + // + // obtained from https://stackoverflow.com/a/69676994/3053548 + const filteredVerdicts: Record = Object.fromEntries(filter.map(k => [k, ProblemVerdictMap[k]])) + + const processedData:VerdictBarChartType[] = [] + + for(const [key, value] of Object.entries(filteredVerdicts)) { + processedData.push({ + name: key.toUpperCase(), + verdict: data[key as keyof Problem] as number, + tooltipTitle: value.title, + fill: ProblemVerdictMap[key].bgHex, + }) + } + + return processedData +} + +////////////////////////////////////////////////////////////////////////////////////////////////// + +export const processSubmissionLanguageRadarChart = ( + data: submissionLangType, +): SubmissionLangType[] => { + const processedData: SubmissionLangType[] = []; + + Object.entries(data).forEach(([key, value]) => { + processedData.push({ + language: Language[key], + count: value, + }); + }); + + return processedData; +}; diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 0377ea1..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,76 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - darkMode: ["class"], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - ], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index a9c22b5..2841e67 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,15 +1,151 @@ import type { Config } from 'tailwindcss' +import colors from 'tailwindcss/colors' import { fontFamily } from "tailwindcss/defaultTheme" const config: Config = { + darkMode: ["class"], content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + // tremor.so tailwindcss config + tremor: { + brand: { + faint: colors.blue[50], + muted: colors.blue[200], + subtle: colors.blue[400], + DEFAULT: colors.blue[500], + emphasis: colors.blue[700], + inverted: colors.white, + }, + background: { + muted: colors.gray[50], + subtle: colors.gray[100], + DEFAULT: colors.white, + emphasis: colors.gray[700], + }, + border: { + DEFAULT: colors.gray[200], + }, + ring: { + DEFAULT: colors.gray[200], + }, + content: { + subtle: colors.gray[400], + DEFAULT: colors.gray[500], + emphasis: colors.gray[700], + strong: colors.gray[900], + inverted: colors.white, + }, + }, + "dark-tremor": { + brand: { + faint: "#0B1229", + muted: colors.blue[950], + subtle: colors.blue[800], + DEFAULT: colors.blue[500], + emphasis: colors.blue[400], + inverted: colors.blue[950], + }, + background: { + muted: "#131A2B", + subtle: colors.gray[800], + DEFAULT: colors.gray[900], + emphasis: colors.gray[300], + }, + border: { + DEFAULT: colors.gray[700], + }, + ring: { + DEFAULT: colors.gray[800], + }, + content: { + subtle: colors.gray[600], + DEFAULT: colors.gray[500], + emphasis: colors.gray[200], + strong: colors.gray[50], + inverted: colors.gray[950], + }, + }, + }, + boxShadow: { + // light + "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", + "tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + "tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + // dark + "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", + "dark-tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + "dark-tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + "tremor-small": "0.375rem", + "tremor-default": "0.5rem", + "tremor-full": "9999px", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', @@ -17,8 +153,15 @@ const config: Config = { fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], }, + fontSize: { + "tremor-label": ["0.75rem", {}], + "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], + "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], + "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], + } }, }, - plugins: [], + plugins: [require("tailwindcss-animate")], } + export default config diff --git a/tsconfig.json b/tsconfig.json index e59724b..af05591 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,