diff --git a/README.md b/README.md index 4ae3bd0..53fd868 100644 --- a/README.md +++ b/README.md @@ -50,5 +50,9 @@ Check out our [Next.js deployment documentation](https://nextjs.org/docs/deploym ![Problems page 2](docs/images/page-problems.png) +### Problems page (`/problems/[problemNum]`) +![Problems num page 1](docs/images/page-problems-num-loading.png) + +![Problems num page 2](docs/images/page-problems-num.png) 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/package-lock.json b/package-lock.json index 2af157d..140d2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.17.1", "@tanstack/react-query-devtools": "^5.17.9", "@tanstack/react-table": "^8.11.6", + "@tanstack/react-virtual": "^3.0.1", "axios": "^1.6.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -25,6 +26,7 @@ "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", + "recharts": "^2.10.3", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" @@ -1973,6 +1975,22 @@ "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", @@ -1985,6 +2003,69 @@ "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", @@ -3381,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", @@ -3416,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", @@ -3524,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", @@ -4157,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", @@ -4198,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", @@ -5009,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", @@ -5706,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", @@ -6319,17 +6543,14 @@ }, "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": { @@ -6346,7 +6567,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6358,13 +6578,11 @@ }, "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": { @@ -6381,7 +6599,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6396,13 +6613,11 @@ }, "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": { @@ -6418,7 +6633,6 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/agent-base": { "version": "7.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6430,7 +6644,6 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/http-proxy-agent": { "version": "7.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6443,7 +6656,6 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.2", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6456,7 +6668,6 @@ }, "node_modules/npm/node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { "version": "8.0.2", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6470,7 +6681,6 @@ }, "node_modules/npm/node_modules/@npmcli/arborist": { "version": "7.2.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6517,7 +6727,6 @@ }, "node_modules/npm/node_modules/@npmcli/config": { "version": "8.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6536,7 +6745,6 @@ }, "node_modules/npm/node_modules/@npmcli/disparity-colors": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6548,7 +6756,6 @@ }, "node_modules/npm/node_modules/@npmcli/fs": { "version": "3.1.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6560,7 +6767,6 @@ }, "node_modules/npm/node_modules/@npmcli/git": { "version": "5.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6579,7 +6785,6 @@ }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { "version": "2.0.2", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6595,7 +6800,6 @@ }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { "version": "3.0.4", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6610,7 +6814,6 @@ }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { "version": "7.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6625,7 +6828,6 @@ }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6634,7 +6836,6 @@ }, "node_modules/npm/node_modules/@npmcli/node-gyp": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6643,7 +6844,6 @@ }, "node_modules/npm/node_modules/@npmcli/package-json": { "version": "5.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6661,7 +6861,6 @@ }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { "version": "7.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6673,7 +6872,6 @@ }, "node_modules/npm/node_modules/@npmcli/query": { "version": "3.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6685,7 +6883,6 @@ }, "node_modules/npm/node_modules/@npmcli/run-script": { "version": "7.0.2", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6701,17 +6898,14 @@ }, "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": { @@ -6723,7 +6917,6 @@ }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { "version": "0.2.1", - "dev": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -6732,7 +6925,6 @@ }, "node_modules/npm/node_modules/@sigstore/sign": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6746,7 +6938,6 @@ }, "node_modules/npm/node_modules/@sigstore/tuf": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -6759,7 +6950,6 @@ }, "node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6768,7 +6958,6 @@ }, "node_modules/npm/node_modules/@tufjs/models": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6781,7 +6970,6 @@ }, "node_modules/npm/node_modules/abbrev": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -6790,7 +6978,6 @@ }, "node_modules/npm/node_modules/abort-controller": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6802,7 +6989,6 @@ }, "node_modules/npm/node_modules/aggregate-error": { "version": "3.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6815,7 +7001,6 @@ }, "node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6824,7 +7009,6 @@ }, "node_modules/npm/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6839,19 +7023,16 @@ }, "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": { @@ -6864,13 +7045,11 @@ }, "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", @@ -6890,7 +7069,6 @@ }, "node_modules/npm/node_modules/bin-links": { "version": "4.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6905,7 +7083,6 @@ }, "node_modules/npm/node_modules/binary-extensions": { "version": "2.2.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6914,7 +7091,6 @@ }, "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6923,7 +7099,6 @@ }, "node_modules/npm/node_modules/buffer": { "version": "6.0.3", - "dev": true, "funding": [ { "type": "github", @@ -6947,7 +7122,6 @@ }, "node_modules/npm/node_modules/builtins": { "version": "5.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -6956,7 +7130,6 @@ }, "node_modules/npm/node_modules/cacache": { "version": "18.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -6979,7 +7152,6 @@ }, "node_modules/npm/node_modules/chalk": { "version": "5.3.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -6991,7 +7163,6 @@ }, "node_modules/npm/node_modules/chownr": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7000,7 +7171,6 @@ }, "node_modules/npm/node_modules/ci-info": { "version": "3.9.0", - "dev": true, "funding": [ { "type": "github", @@ -7015,7 +7185,6 @@ }, "node_modules/npm/node_modules/cidr-regex": { "version": "3.1.1", - "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7027,7 +7196,6 @@ }, "node_modules/npm/node_modules/clean-stack": { "version": "2.2.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7036,7 +7204,6 @@ }, "node_modules/npm/node_modules/cli-columns": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7049,7 +7216,6 @@ }, "node_modules/npm/node_modules/cli-table3": { "version": "0.6.3", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7064,7 +7230,6 @@ }, "node_modules/npm/node_modules/clone": { "version": "1.0.4", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7073,7 +7238,6 @@ }, "node_modules/npm/node_modules/cmd-shim": { "version": "6.0.2", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7082,7 +7246,6 @@ }, "node_modules/npm/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7094,13 +7257,11 @@ }, "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": { @@ -7109,7 +7270,6 @@ }, "node_modules/npm/node_modules/columnify": { "version": "1.6.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7122,19 +7282,16 @@ }, "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": { @@ -7148,7 +7305,6 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7163,7 +7319,6 @@ }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -7175,7 +7330,6 @@ }, "node_modules/npm/node_modules/debug": { "version": "4.3.4", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7192,13 +7346,11 @@ }, "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": { @@ -7210,13 +7362,11 @@ }, "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": { @@ -7225,29 +7375,24 @@ }, "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": { @@ -7256,13 +7401,11 @@ }, "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": { @@ -7271,7 +7414,6 @@ }, "node_modules/npm/node_modules/events": { "version": "3.3.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7280,13 +7422,11 @@ }, "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": { @@ -7295,7 +7435,6 @@ }, "node_modules/npm/node_modules/foreground-child": { "version": "3.1.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7311,7 +7450,6 @@ }, "node_modules/npm/node_modules/fs-minipass": { "version": "3.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7323,13 +7461,11 @@ }, "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": { @@ -7348,7 +7484,6 @@ }, "node_modules/npm/node_modules/glob": { "version": "10.3.10", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7370,13 +7505,11 @@ }, "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": { @@ -7388,13 +7521,11 @@ }, "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": { @@ -7406,16 +7537,13 @@ }, "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" }, @@ -7425,7 +7553,6 @@ }, "node_modules/npm/node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -7445,7 +7572,6 @@ }, "node_modules/npm/node_modules/ignore-walk": { "version": "6.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7457,7 +7583,6 @@ }, "node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7466,7 +7591,6 @@ }, "node_modules/npm/node_modules/indent-string": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7475,7 +7599,6 @@ }, "node_modules/npm/node_modules/ini": { "version": "4.1.1", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7484,7 +7607,6 @@ }, "node_modules/npm/node_modules/init-package-json": { "version": "6.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7502,13 +7624,11 @@ }, "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": { @@ -7517,7 +7637,6 @@ }, "node_modules/npm/node_modules/is-cidr": { "version": "4.0.2", - "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -7529,7 +7648,6 @@ }, "node_modules/npm/node_modules/is-core-module": { "version": "2.13.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7541,7 +7659,6 @@ }, "node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7550,19 +7667,16 @@ }, "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": { @@ -7580,7 +7694,6 @@ }, "node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -7589,7 +7702,6 @@ }, "node_modules/npm/node_modules/json-stringify-nice": { "version": "1.1.4", - "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -7598,7 +7710,6 @@ }, "node_modules/npm/node_modules/jsonparse": { "version": "1.3.1", - "dev": true, "engines": [ "node >= 0.2.0" ], @@ -7607,19 +7718,16 @@ }, "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": { @@ -7632,7 +7740,6 @@ }, "node_modules/npm/node_modules/libnpmdiff": { "version": "6.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7652,7 +7759,6 @@ }, "node_modules/npm/node_modules/libnpmexec": { "version": "7.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7674,7 +7780,6 @@ }, "node_modules/npm/node_modules/libnpmfund": { "version": "5.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7686,7 +7791,6 @@ }, "node_modules/npm/node_modules/libnpmhook": { "version": "10.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7699,7 +7803,6 @@ }, "node_modules/npm/node_modules/libnpmorg": { "version": "6.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7712,7 +7815,6 @@ }, "node_modules/npm/node_modules/libnpmpack": { "version": "6.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7727,7 +7829,6 @@ }, "node_modules/npm/node_modules/libnpmpublish": { "version": "9.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7746,7 +7847,6 @@ }, "node_modules/npm/node_modules/libnpmsearch": { "version": "7.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7758,7 +7858,6 @@ }, "node_modules/npm/node_modules/libnpmteam": { "version": "6.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7771,7 +7870,6 @@ }, "node_modules/npm/node_modules/libnpmversion": { "version": "5.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7787,7 +7885,6 @@ }, "node_modules/npm/node_modules/lru-cache": { "version": "10.0.1", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7796,7 +7893,6 @@ }, "node_modules/npm/node_modules/make-fetch-happen": { "version": "13.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7818,7 +7914,6 @@ }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7833,7 +7928,6 @@ }, "node_modules/npm/node_modules/minipass": { "version": "7.0.4", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -7842,7 +7936,6 @@ }, "node_modules/npm/node_modules/minipass-collect": { "version": "1.0.2", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7854,7 +7947,6 @@ }, "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7866,7 +7958,6 @@ }, "node_modules/npm/node_modules/minipass-fetch": { "version": "3.0.4", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7883,7 +7974,6 @@ }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7895,7 +7985,6 @@ }, "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7907,7 +7996,6 @@ }, "node_modules/npm/node_modules/minipass-json-stream": { "version": "1.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7917,7 +8005,6 @@ }, "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7929,7 +8016,6 @@ }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7941,7 +8027,6 @@ }, "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7953,7 +8038,6 @@ }, "node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7965,7 +8049,6 @@ }, "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -7977,7 +8060,6 @@ }, "node_modules/npm/node_modules/minizlib": { "version": "2.1.2", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -7990,7 +8072,6 @@ }, "node_modules/npm/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8002,7 +8083,6 @@ }, "node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", - "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -8014,13 +8094,11 @@ }, "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": { @@ -8029,7 +8107,6 @@ }, "node_modules/npm/node_modules/negotiator": { "version": "0.6.3", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8038,7 +8115,6 @@ }, "node_modules/npm/node_modules/node-gyp": { "version": "10.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8062,7 +8138,6 @@ }, "node_modules/npm/node_modules/nopt": { "version": "7.2.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8077,7 +8152,6 @@ }, "node_modules/npm/node_modules/normalize-package-data": { "version": "6.0.0", - "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -8092,7 +8166,6 @@ }, "node_modules/npm/node_modules/npm-audit-report": { "version": "5.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8101,7 +8174,6 @@ }, "node_modules/npm/node_modules/npm-bundled": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8113,7 +8185,6 @@ }, "node_modules/npm/node_modules/npm-install-checks": { "version": "6.3.0", - "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -8125,7 +8196,6 @@ }, "node_modules/npm/node_modules/npm-normalize-package-bin": { "version": "3.0.1", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8134,7 +8204,6 @@ }, "node_modules/npm/node_modules/npm-package-arg": { "version": "11.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8149,7 +8218,6 @@ }, "node_modules/npm/node_modules/npm-packlist": { "version": "8.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8161,7 +8229,6 @@ }, "node_modules/npm/node_modules/npm-pick-manifest": { "version": "9.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8176,7 +8243,6 @@ }, "node_modules/npm/node_modules/npm-profile": { "version": "9.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8189,7 +8255,6 @@ }, "node_modules/npm/node_modules/npm-registry-fetch": { "version": "16.1.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8207,7 +8272,6 @@ }, "node_modules/npm/node_modules/npm-user-validate": { "version": "2.0.0", - "dev": true, "inBundle": true, "license": "BSD-2-Clause", "engines": { @@ -8216,7 +8280,6 @@ }, "node_modules/npm/node_modules/npmlog": { "version": "7.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8231,7 +8294,6 @@ }, "node_modules/npm/node_modules/p-map": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8246,7 +8308,6 @@ }, "node_modules/npm/node_modules/pacote": { "version": "17.0.4", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8278,7 +8339,6 @@ }, "node_modules/npm/node_modules/parse-conflict-json": { "version": "3.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8292,7 +8352,6 @@ }, "node_modules/npm/node_modules/path-key": { "version": "3.1.1", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8301,7 +8360,6 @@ }, "node_modules/npm/node_modules/path-scurry": { "version": "1.10.1", - "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8317,7 +8375,6 @@ }, "node_modules/npm/node_modules/postcss-selector-parser": { "version": "6.0.13", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8330,7 +8387,6 @@ }, "node_modules/npm/node_modules/proc-log": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8339,7 +8395,6 @@ }, "node_modules/npm/node_modules/process": { "version": "0.11.10", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8348,7 +8403,6 @@ }, "node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", - "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -8357,7 +8411,6 @@ }, "node_modules/npm/node_modules/promise-call-limit": { "version": "1.0.2", - "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -8366,13 +8419,11 @@ }, "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": { @@ -8385,7 +8436,6 @@ }, "node_modules/npm/node_modules/promzard": { "version": "1.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8397,7 +8447,6 @@ }, "node_modules/npm/node_modules/qrcode-terminal": { "version": "0.12.0", - "dev": true, "inBundle": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" @@ -8405,7 +8454,6 @@ }, "node_modules/npm/node_modules/read": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8417,7 +8465,6 @@ }, "node_modules/npm/node_modules/read-cmd-shim": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8426,7 +8473,6 @@ }, "node_modules/npm/node_modules/read-package-json": { "version": "7.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8441,7 +8487,6 @@ }, "node_modules/npm/node_modules/read-package-json-fast": { "version": "3.0.2", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8454,7 +8499,6 @@ }, "node_modules/npm/node_modules/readable-stream": { "version": "4.4.2", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8470,7 +8514,6 @@ }, "node_modules/npm/node_modules/retry": { "version": "0.12.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8479,7 +8522,6 @@ }, "node_modules/npm/node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, "funding": [ { "type": "github", @@ -8499,14 +8541,11 @@ }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/npm/node_modules/semver": { "version": "7.5.4", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8521,7 +8560,6 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8533,13 +8571,11 @@ }, "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": { @@ -8551,7 +8587,6 @@ }, "node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8560,7 +8595,6 @@ }, "node_modules/npm/node_modules/signal-exit": { "version": "4.0.2", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8572,7 +8606,6 @@ }, "node_modules/npm/node_modules/sigstore": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8587,7 +8620,6 @@ }, "node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8597,7 +8629,6 @@ }, "node_modules/npm/node_modules/socks": { "version": "2.7.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8611,7 +8642,6 @@ }, "node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", - "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -8621,13 +8651,11 @@ }, "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": { @@ -8637,13 +8665,11 @@ }, "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": { @@ -8655,7 +8681,6 @@ }, "node_modules/npm/node_modules/string_decoder": { "version": "1.3.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8664,7 +8689,6 @@ }, "node_modules/npm/node_modules/string-width": { "version": "4.2.3", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8679,7 +8703,6 @@ "node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8693,7 +8716,6 @@ }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8706,7 +8728,6 @@ "node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8718,7 +8739,6 @@ }, "node_modules/npm/node_modules/supports-color": { "version": "9.4.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8730,7 +8750,6 @@ }, "node_modules/npm/node_modules/tar": { "version": "6.2.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8747,7 +8766,6 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8759,7 +8777,6 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8771,7 +8788,6 @@ }, "node_modules/npm/node_modules/tar/node_modules/minipass": { "version": "5.0.0", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8780,19 +8796,16 @@ }, "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": { @@ -8801,7 +8814,6 @@ }, "node_modules/npm/node_modules/tuf-js": { "version": "2.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8815,7 +8827,6 @@ }, "node_modules/npm/node_modules/unique-filename": { "version": "3.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8827,7 +8838,6 @@ }, "node_modules/npm/node_modules/unique-slug": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8839,13 +8849,11 @@ }, "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": { @@ -8855,7 +8863,6 @@ }, "node_modules/npm/node_modules/validate-npm-package-name": { "version": "5.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8867,13 +8874,11 @@ }, "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": { @@ -8882,7 +8887,6 @@ }, "node_modules/npm/node_modules/which": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8897,7 +8901,6 @@ }, "node_modules/npm/node_modules/which/node_modules/isexe": { "version": "3.1.1", - "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -8906,7 +8909,6 @@ }, "node_modules/npm/node_modules/wide-align": { "version": "1.1.5", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -8915,7 +8917,6 @@ }, "node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8933,7 +8934,6 @@ "node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -8950,7 +8950,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8962,7 +8961,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -8974,13 +8972,11 @@ }, "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": { @@ -8997,7 +8993,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", - "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -9012,7 +9007,6 @@ }, "node_modules/npm/node_modules/write-file-atomic": { "version": "5.0.1", - "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -9025,7 +9019,6 @@ }, "node_modules/npm/node_modules/yallist": { "version": "4.0.0", - "dev": true, "inBundle": true, "license": "ISC" }, @@ -9655,7 +9648,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", @@ -9751,8 +9743,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", @@ -9799,6 +9795,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", @@ -9821,6 +9831,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", @@ -9948,6 +9973,37 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.3.tgz", + "integrity": "sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==", + "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", @@ -10922,6 +10978,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", @@ -11285,6 +11346,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", diff --git a/package.json b/package.json index ad55bc6..88420a6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "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", @@ -17,6 +19,7 @@ "@tanstack/react-query": "^5.17.1", "@tanstack/react-query-devtools": "^5.17.9", "@tanstack/react-table": "^8.11.6", + "@tanstack/react-virtual": "^3.0.1", "axios": "^1.6.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -26,6 +29,7 @@ "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", + "recharts": "^2.10.3", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" diff --git a/src/app/api/poll/[pollId]/route.ts b/src/app/api/poll/[pollId]/route.ts index d346249..b78df8a 100644 --- a/src/app/api/poll/[pollId]/route.ts +++ b/src/app/api/poll/[pollId]/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import axios from "axios"; -import { Language, Problem, Submission, Verdict } from "@/types"; +import { Language, Problem, Submission, ProblemVerdictMap } from "@/types"; import { fetchLiveSubmissionsUrl, uhuntProblemIdUrl } from "@/utils/constants"; type getParamsType = { @@ -38,10 +38,12 @@ export const GET = async (_request: Request, { params }: getParamsType) => { const converted = data .map(async (submission: Submission) => { - submission.msg.verdict = Verdict[submission.msg.ver] || { - fgColor: "", - bgColor: "", + 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] || "--"; diff --git a/src/app/api/problems/[problemNum]/route.ts b/src/app/api/problems/[problemNum]/route.ts index ebd4d4b..fc3ce22 100644 --- a/src/app/api/problems/[problemNum]/route.ts +++ b/src/app/api/problems/[problemNum]/route.ts @@ -3,19 +3,13 @@ import { z } from "zod"; import { uhuntProblemNumUrl } from "@/utils/constants"; import { Problem, ProblemStatus } from "@/types"; +import { problemNumSchema as schema } from "@/schema"; type getParamsType = { - params: { - problemNum: string; - }; + params: z.infer; }; export const GET = async (_request: Request, { params }: getParamsType) => { - const schema = z.object({ - problemNum: z.coerce - .number({ invalid_type_error: "Must be a number" }) - .min(1, "Must be a number greater than 0"), - }); const schemaResponse = await schema.safeParseAsync(params); if (!schemaResponse.success) { @@ -34,7 +28,17 @@ export const GET = async (_request: Request, { params }: getParamsType) => { const response = await fetch(url); const data: Problem = await response.json(); - data.status = ProblemStatus[data.status as unknown as number] + + 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/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..3824b98 --- /dev/null +++ b/src/app/api/submissions/language/[problemNum]/route.tsx @@ -0,0 +1,76 @@ +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 } 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 responseData: getResponseType = submissionData.reduce((acc, cur) => { + const languageId = cur.lan; + acc[languageId] = acc[languageId] + 1; + + return acc; + }, languageObj); + delete responseData["undefined"]; + + return Response.json(responseData); +}; 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..0fd1cd1 --- /dev/null +++ b/src/app/api/submissions/overtime/[problemNum]/route.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; + +import { submissionOvertimeSchema as schema } from "@/schema"; +import { NextResponse } from "next/server"; +import { uhuntProblemNumUrl, uhuntSubmissionCountUrl } from "@/utils/constants"; +import { Problem } from "@/types"; +import moment, { Moment } from "moment"; + +type getParamsType = { + params: z.infer; +}; + +export type getResponseType = { + name: string; + time: string; // time formatted to year + submissions: number; + fill: string; +} + +/** + * 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:getResponseType[] = 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/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..9eeee8b --- /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 } 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/page.tsx b/src/app/problems/page.tsx index a1d05d6..30764b2 100644 --- a/src/app/problems/page.tsx +++ b/src/app/problems/page.tsx @@ -4,6 +4,7 @@ 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(); @@ -24,7 +25,7 @@ const ProblemsPage = () => { return (

All Problems

- +
); }; diff --git a/src/components/charts/ProblemVerdictChart.tsx b/src/components/charts/ProblemVerdictChart.tsx new file mode 100644 index 0000000..f65ef7e --- /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 { processedProblemVerdictBarChartType } from "@/utils/dataProcessing"; +import ChartTooltip from "@/components/charts/Tooltip"; + +type Props = { + data: processedProblemVerdictBarChartType[]; +}; + +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/SubmissionLanguageRadarChart.tsx b/src/components/charts/SubmissionLanguageRadarChart.tsx new file mode 100644 index 0000000..21c3a29 --- /dev/null +++ b/src/components/charts/SubmissionLanguageRadarChart.tsx @@ -0,0 +1,63 @@ +import { + Radar, + RadarChart, + PolarAngleAxis, + PolarGrid, + ResponsiveContainer, + Tooltip, +} from "recharts"; +import { useTheme } from "next-themes"; + +import ChartTooltip from "@/components/charts/Tooltip"; +import { processSubmissionLanguageRadarChart } from "@/utils/dataProcessing"; +import { getResponseType } from "@/app/api/submissions/language/[problemNum]/route"; + +type Props = { + data: getResponseType; +}; + +const SubmissionLanguageRadarChart = ({ data }: Props) => { + const { theme } = useTheme(); + const processedData = processSubmissionLanguageRadarChart(data); + + 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..e1c0777 --- /dev/null +++ b/src/components/charts/SubmissionsOvertimeChart.tsx @@ -0,0 +1,58 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, +} from "recharts"; + +import { getResponseType as SubmissionOvertimeType } from "@/app/api/submissions/overtime/[problemNum]/route"; +import ChartTooltip from "@/components/charts/Tooltip"; + +type Props = { + data: SubmissionOvertimeType[]; +}; + +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..e360e7d --- /dev/null +++ b/src/components/charts/Tooltip.tsx @@ -0,0 +1,155 @@ +/* + * 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/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/index.tsx b/src/components/ui/data-table/index.tsx index 30c1f7b..8bae903 100644 --- a/src/components/ui/data-table/index.tsx +++ b/src/components/ui/data-table/index.tsx @@ -37,11 +37,16 @@ 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({}) @@ -128,7 +133,7 @@ export function DataTable({
-
+
{table.getHeaderGroups().map((headerGroup) => ( 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 index 4502133..e6f5122 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { useQuery } from "@tanstack/react-query"; -import { Submission } from "@/types"; +import { Problem, Submission } from "@/types"; /** * Enum for React Query Keys when using React-query @@ -12,6 +12,26 @@ export enum queryKey { * 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", } /** @@ -29,7 +49,7 @@ export const useFetchLiveSubmission = (pollId = 0, fetchInterval = 5000) => { return data; }, refetchInterval: fetchInterval, - staleTime: fetchInterval + staleTime: fetchInterval, }); }; @@ -39,8 +59,75 @@ export const useFetchLiveSubmission = (pollId = 0, fetchInterval = 5000) => { export const useFetchProblems = () => { return useQuery({ queryKey: [queryKey.allProblems], - queryFn: async () => axios.get("/api/problems").then((res) => res.data), - refetchOnWindowFocus: false + queryFn: async () => axios.get("/api/problems").then((res) => res.data), + refetchOnWindowFocus: 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, + }); +}; diff --git a/src/schema/index.ts b/src/schema/index.ts new file mode 100644 index 0000000..c5cbe3f --- /dev/null +++ b/src/schema/index.ts @@ -0,0 +1,34 @@ +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"), +}) diff --git a/src/types/index.ts b/src/types/index.ts index a835193..14aecb6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,192 @@ +/* + * 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; +} + +const ProblemVerdictMap: Record = { + /** + * Number of Accepted + */ + ac: { + title: "Accepted", + bgColor: "bg-[#00AA00]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#00AA00", + fgHex: "", + }, + /** + * Number of Presentation Error + */ + pe: { + title: "Presentation error", + bgColor: "bg-[#666600]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#666600", + fgHex: "", + }, + /** + * Number of Wrong Answer + */ + wa: { + title: "Wrong answer", + bgColor: "bg-[#FF0000]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#FF0000", + fgHex: "", + }, + /** + * Number of Time Limit Exceeded + */ + tle: { + title: "Time limit exceeded", + bgColor: "bg-[#0000FF]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#0000FF", + fgHex: "", + }, + /** + * Number of Memory Limit Exceeded + */ + mle: { + title: "Memory limit exceeded", + bgColor: "bg-[#0000AA]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#0000AA", + fgHex: "", + }, + /** + * Number of Compilation Error + */ + ce: { + title: "Compile error", + bgColor: "bg-orange-600", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#EA5A0C", + fgHex: "", + }, + /** + * Number of Runtime Error + */ + re: { + title: "Runtime error", + bgColor: "bg-[#00AAAA]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#00AAAA", + fgHex: "", + }, + /** + * Number of Output Limit Exceeded + */ + ole: { + title: "Output limit exceeded", + bgColor: "bg-[#000066]", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "#000066", + fgHex: "", + }, + /** + * Number of Submission Error + */ + sube: { + title: "Submission Error", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "6b7280", + fgHex: "", + }, + /** + * 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: "", + }, + /** + * Number of In Queue + */ + inq: { + title: "- In Queue -", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "6b7280", + fgHex: "", + }, + /** + * Number of Restricted Function + */ + rf: { + title: "Restricted function", + bgColor: "bg-gray-500", + fgColor: "text-primary-foreground dark:text-secondary-foreground", + bgHex: "6b7280", + fgHex: "", + } +} +// 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` * @@ -97,36 +280,7 @@ export type Problem = { rej: number; }; -export type VerdictType = { - /** - * Tailwindcss styles for foreground - */ - fgColor: string; - /** - * Tailwindcss styles for background - */ - bgColor: string; - /** - * Title to be displayed on the front end - */ - title: string; -}; - -export const Verdict: Record = { - 0 : { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-gray-500", title: "- In Queue -" }, - 10: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-gray-500", title: "Submission error" }, - 15: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-gray-500", title: "Can't be judged" }, - 20: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-gray-500", title: "- In Queue -" }, - 30: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-orange-600", title: "Compile error" }, - 35: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-gray-500", title: "Restricted function" }, - 40: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#00AAAA]", title: "Runtime error" }, - 45: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#000066]", title: "Output limit" }, - 50: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#0000FF]", title: "Time limit" }, - 60: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#0000AA]", title: "Memory limit" }, - 70: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#FF0000]", title: "Wrong answer" }, - 80: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#666600]", title: "Presentation error" }, - 90: { fgColor: "text-primary-foreground dark:text-secondary-foreground", bgColor: "bg-[#00AA00]", title: "Accepted" }, -}; +/////////////////////////////////////////////////////////////////////////////////////////////////// export const Language: Record = { 1: "ANSI C", @@ -134,8 +288,11 @@ export const Language: Record = { 3: "C++", 4: "Pascal", 5: "C++11", + 6: "Python", }; +/////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Submission data structure * @@ -176,15 +333,13 @@ export type Submission = { * Verdict properties * * contains - * - verdict title Ex: `Accepted`, or `Compile error` + * - 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: { - title: string; - fgColor: string; - bgColor: string; - }; + verdict: ProblemVerdictType; /** * Language ID */ @@ -215,3 +370,5 @@ export type Submission = { uname: string; }; }; + +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 618f01b..b481ea5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -15,9 +15,9 @@ export const viewProblemUrl = /** * URL for viewing the details of the problem PDF using problem ID - * @param {String} pid problem ID + * @param {String | Number} pid problem ID */ -export const uhuntViewProblemUrl = (pid: string) => `${viewProblemUrl}${pid}`; +export const uhuntViewProblemUrl = (pid: string | number) => `${viewProblemUrl}${pid}`; /** * URL for fetching all problems on uva-uhunt @@ -26,25 +26,25 @@ export const uhuntAllProblemsUrl = () => 'https://uhunt.onlinejudge.org/api/p'; /** * URL for getting Problem data using Problem ID - * @param {String} pid - problem ID + * @param {String | Number} pid - problem ID */ -export const uhuntProblemIdUrl = (pid: string) => +export const uhuntProblemIdUrl = (pid: string | number) => `${uhuntBaseApiUrl}/p/id/${pid}`; /** * URL for getting Problem data using Problem number - * @param {String} problemNumber - problem number + * @param {String | Number} problemNumber - problem number */ -export const uhuntProblemNumUrl = (problemNumber: string) => +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} pid - Problem ID + * @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, start = 1, count = 100) => +export const uhuntProblemRankUrl = (pid: string | number, start = 1, count = 100) => `${uhuntBaseApiUrl}/p/rank/${pid}/${start}/${count}`; /** @@ -53,13 +53,13 @@ export const uhuntProblemRankUrl = (pid: string, start = 1, count = 100) => * 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} pid - problem ID + * @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, + pid: string | number, submissionTime = moment().unix(), back = 20, jump = 12, @@ -74,16 +74,25 @@ export const uhuntUsername2UidUrl = (username: string) => /** * URL for getting Submission list of a problem using Problem ID - * @param {String} pid - 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) => - `${uhuntBaseApiUrl}/p/subs/${pid}/0/${moment().unix()}`; +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 {Number} uid User ID + * @param {String | Number} uid User ID */ -export const uhuntUserSubmissionsUrl = (uid: string) => +export const uhuntUserSubmissionsUrl = (uid: string | number) => `${uhuntBaseApiUrl}/subs-user/${uid}`; /** diff --git a/src/utils/dataProcessing.ts b/src/utils/dataProcessing.ts new file mode 100644 index 0000000..9d34205 --- /dev/null +++ b/src/utils/dataProcessing.ts @@ -0,0 +1,69 @@ +import { Language, Problem, ProblemVerdictMap, ProblemVerdictType } from "@/types"; +import {getResponseType as submissionLangType} from '@/app/api/submissions/language/[problemNum]/route' + +export type processedProblemVerdictBarChartType = { + /** + * 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; +}; +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:processedProblemVerdictBarChartType[] = [] + + 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 type processedSubmissionLangType = { + language: string; + count: number; +} + +export const processSubmissionLanguageRadarChart = ( + data: submissionLangType, +): processedSubmissionLangType[] => { + const processedData: processedSubmissionLangType[] = []; + + Object.entries(data).forEach(([key, value]) => { + processedData.push({ + language: Language[key], + count: value, + }); + }); + + return processedData; +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index a9d9eae..2841e67 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from 'tailwindcss' +import colors from 'tailwindcss/colors' import { fontFamily } from "tailwindcss/defaultTheme" const config: Config = { @@ -52,11 +53,83 @@ const config: Config = { 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": { @@ -80,6 +153,12 @@ 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: [require("tailwindcss-animate")],