diff --git a/.gitignore b/.gitignore index c12db7a..05a99e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Secrets*.toml backups/ .env *.log + diff --git a/Cargo.lock b/Cargo.lock index c00b3f1..e0ae6a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -88,8 +88,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfff2b17d272a5e3e201feda444e2c24b011fa722951268d1bd8b9b5bc6dc449" dependencies = [ "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", + "async-graphql-parser 7.0.15", + "async-graphql-value 7.0.15", "async-stream", "async-trait", "base64 0.22.1", @@ -101,7 +101,7 @@ dependencies = [ "futures-util", "handlebars", "http", - "indexmap", + "indexmap 2.7.1", "mime", "multer", "num-traits", @@ -139,7 +139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8e5d0c6697def2f79ccbd972fb106b633173a6066e430b480e1ff9376a7561a" dependencies = [ "Inflector", - "async-graphql-parser", + "async-graphql-parser 7.0.15", "darling", "proc-macro-crate", "proc-macro2", @@ -149,18 +149,43 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "async-graphql-parser" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99841c1f890fda6712054e7e37b207738f4aa97870cb1bffcab2f09f2df0957a" +dependencies = [ + "async-graphql-value 2.11.3", + "pest", + "pest_derive", + "serde", + "serde_json", +] + [[package]] name = "async-graphql-parser" version = "7.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8531ee6d292c26df31c18c565ff22371e7bdfffe7f5e62b69537db0b8fd554dc" dependencies = [ - "async-graphql-value", + "async-graphql-value 7.0.15", "pest", "serde", "serde_json", ] +[[package]] +name = "async-graphql-value" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cecac7ab6737364cff7b16e9273dd51fac7cfbd14ab5d84127df5a56ca9d422" +dependencies = [ + "bytes", + "indexmap 1.9.3", + "serde", + "serde_json", +] + [[package]] name = "async-graphql-value" version = "7.0.15" @@ -168,7 +193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "741110dda927420a28fbc1c310543d3416f789a6ba96859c2c265843a0a96887" dependencies = [ "bytes", - "indexmap", + "indexmap 2.7.1", "serde", "serde_json", ] @@ -904,7 +929,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -925,6 +950,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1289,6 +1320,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.7.1" @@ -2034,6 +2076,7 @@ version = "0.1.0" dependencies = [ "async-graphql", "async-graphql-axum", + "async-graphql-parser 2.11.3", "axum", "chrono", "chrono-tz", @@ -2387,7 +2430,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.2", "hashlink 0.10.0", - "indexmap", + "indexmap 2.7.1", "log", "memchr", "once_cell", @@ -2896,7 +2939,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", diff --git a/Cargo.toml b/Cargo.toml index 5002d3c..819ac1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] async-graphql = { version = "7.0.15", features = ["chrono"] } async-graphql-axum = "7.0.6" +async-graphql-parser = "2.0" axum = "0.8.1" chrono = { version = "0.4.38", features = ["clock"] } serde = { version = "1.0.188", features = ["derive"] } diff --git a/migrations/20250312124630_add_leaderboard_tables.sql b/migrations/20250312124630_add_leaderboard_tables.sql new file mode 100644 index 0000000..89ff540 --- /dev/null +++ b/migrations/20250312124630_add_leaderboard_tables.sql @@ -0,0 +1,38 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS leaderboard ( + id SERIAL PRIMARY KEY, + member_id INT UNIQUE NOT NULL, + leetcode_score INT, + codeforces_score INT, + unified_score INT NOT NULL, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (member_id) REFERENCES member(member_id) +); + +CREATE TABLE IF NOT EXISTS leetcode_stats ( + id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + leetcode_username VARCHAR(255) NOT NULL, + problems_solved INT NOT NULL, + easy_solved INT NOT NULL, + medium_solved INT NOT NULL, + hard_solved INT NOT NULL, + contests_participated INT NOT NULL, + best_rank INT NOT NULL, + total_contests INT NOT NULL, + FOREIGN KEY (member_id) REFERENCES member(member_id) +); + +CREATE TABLE IF NOT EXISTS codeforces_stats ( + id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + codeforces_handle VARCHAR(255) NOT NULL, + codeforces_rating INT NOT NULL, + max_rating INT NOT NULL, + contests_participated INT NOT NULL, + FOREIGN KEY (member_id) REFERENCES member(member_id) +); + +ALTER TABLE leetcode_stats ADD CONSTRAINT leetcode_stats_member_id_key UNIQUE (member_id); +ALTER TABLE codeforces_stats ADD CONSTRAINT codeforces_stats_member_id_key UNIQUE (member_id); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e0641ff --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "root", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@apollo/client": "^3.13.4", + "graphiql": "^3.8.3", + "graphql": "^16.10.0" + } + }, + "node_modules/@apollo/client": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.4.tgz", + "integrity": "sha512-Ot3RaN2M/rhIKDqXBdOVlN0dQbHydUrYJ9lTxkvd6x7W1pAjwduUccfoz2gsO4U9by7oWtRj/ySF0MFNUp+9Aw==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.18.0", + "prop-types": "^15.7.2", + "rehackt": "^0.1.0", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, + "node_modules/@codemirror/language": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", + "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.4", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz", + "integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@graphiql/react": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.28.2.tgz", + "integrity": "sha512-6PE2Ff9dXpyQMHy3oKzCjT738kY2+wdQ4Xce8+1K+G2yMGZ716Fo0i4vW3S6ErHVI4S/C76gFTQlv/pzxU5ylg==", + "dependencies": { + "@graphiql/toolkit": "^0.11.0", + "@headlessui/react": "^1.7.15", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-tooltip": "^1.0.6", + "@radix-ui/react-visually-hidden": "^1.0.3", + "@types/codemirror": "^5.60.8", + "clsx": "^1.2.1", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.2.0", + "copy-to-clipboard": "^3.2.0", + "framer-motion": "^6.5.1", + "get-value": "^3.0.1", + "graphql-language-service": "^5.3.0", + "markdown-it": "^14.1.0", + "react-compiler-runtime": "19.0.0-beta-37ed2a7-20241206", + "set-value": "^4.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@graphiql/toolkit": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.11.1.tgz", + "integrity": "sha512-G02te70/oYYna5UhbH6TXwNxeQyWa+ChlPonUrKwC5Ot9ItraGJ9yUU4sS+YRaA+EvkzNoHG79XcW2k1QaAMiw==", + "dependencies": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "graphql-ws": ">= 4.5.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + } + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "peer": true + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "peer": true + }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", + "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "dependencies": { + "@motionone/animation": "^10.12.0", + "@motionone/generators": "^10.12.0", + "@motionone/types": "^10.12.0", + "@motionone/utils": "^10.12.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.4.tgz", + "integrity": "sha512-jPWC3BXvVLHsMX67NEHpJaZ+/FySoNxFfBEiF4GBc1+/nVwdRm+UcSCYnKP3pXQr0eEsDpXi/PQZhNfJNopH0g==", + "dependencies": { + "@tanstack/virtual-core": "3.13.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.4.tgz", + "integrity": "sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/codemirror": { + "version": "5.65.18", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", + "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==" + }, + "node_modules/codemirror-graphql": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.2.0.tgz", + "integrity": "sha512-egIiewf5zEH5LLSkJpJDpYxO1OkNruD0gTWiBrS1JmXk7yjt5WPw7jSmDRkWJx8JheHONltaJPNPWdTUT5LRIQ==", + "dependencies": { + "@types/codemirror": "^0.0.90", + "graphql-language-service": "5.3.0" + }, + "peerDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/@types/codemirror": { + "version": "0.0.90", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.90.tgz", + "integrity": "sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA==", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/debounce-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "dependencies": { + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/framesync": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", + "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/graphiql": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.8.3.tgz", + "integrity": "sha512-cuPDYtXVKV86Pu5PHBX642Odi/uVEE2y1Jxq5rGO/Qy1G2lRp7ZZ7a/T30RzxhuLSWo9zUbzq0P3U49//H0Ugw==", + "dependencies": { + "@graphiql/react": "^0.28.2" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-language-service": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.3.0.tgz", + "integrity": "sha512-gCQIIy7lM9CB1KPLEb+DNZLczA9zuTLEOJE2hEQZTFYInogdmMDRa6RAkvM4LL0LcgcS+3uPs6KtHlcjCqRbUg==", + "dependencies": { + "debounce-promise": "^3.1.2", + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/meros": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz", + "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, + "node_modules/popmotion": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", + "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", + "dependencies": { + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-compiler-runtime": { + "version": "19.0.0-beta-37ed2a7-20241206", + "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.0.0-beta-37ed2a7-20241206.tgz", + "integrity": "sha512-9e6rCpVylr9EnVocgYAjft7+2v01BDpajeHKRoO+oc9pKcAMTpstHtHvE/TSVbyf4FvzCGjfKcfHM9XGTXI6Tw==", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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==" + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/rehackt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "peer": true + }, + "node_modules/style-value-types": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", + "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "peer": true + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "dependencies": { + "zen-observable": "0.8.15" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9858870 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@apollo/client": "^3.13.4", + "graphiql": "^3.8.3", + "graphql": "^16.10.0" + } +} diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index d342f87..4ec0699 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -4,8 +4,14 @@ use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tracing::{debug, error, info}; +use crate::graphql::mutations::{fetch_and_update_codeforces_stats,fetch_and_update_leetcode,update_leaderboard_scores}; -use crate::models::member::Member; +use crate::{ + models::{ + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + }, +}; pub async fn run_daily_task_at_midnight(pool: Arc) { loop { @@ -44,12 +50,87 @@ async fn execute_daily_task(pool: Arc) { .await; match members { - Ok(members) => update_attendance(members, &pool).await, + Ok(members) => { + update_attendance(members, &pool).await; + update_leaderboard_task(pool.clone()).await; + } // TODO: Handle this Err(e) => error!("Failed to fetch members: {:?}", e), }; } +pub async fn update_leaderboard_task(pool: Arc) { + #[allow(deprecated)] + let today = chrono::Utc::now() + .with_timezone(&Kolkata) + .date() + .naive_local(); + debug!("Updating leaderboard on {}", today); + + let members: Result, sqlx::Error> = + sqlx::query_as::<_, Member>("SELECT * FROM Member") + .fetch_all(pool.as_ref()) + .await; + + match members { + Ok(members) => { + for member in members { + // Fetch LeetCode username + let leetcode_username = sqlx::query_as::<_, LeetCodeStats>( + "SELECT leetcode_username FROM leetcode_stats WHERE member_id = $1", + ) + .bind(member.member_id) + .fetch_optional(pool.as_ref()) + .await; + + if let Ok(Some(leetcode_stats)) = leetcode_username { + let username = leetcode_stats.leetcode_username.clone(); + + // Fetch and update LeetCode stats + match fetch_and_update_leetcode(pool.clone(), member.member_id, &username).await { + Ok(_) => println!("LeetCode stats updated for member ID: {}", member.member_id), + Err(e) => eprintln!( + "Failed to update LeetCode stats for member ID {}: {:?}", + member.member_id, e + ), + } + } + + // Fetch Codeforces username + let codeforces_username = sqlx::query_as::<_, CodeforcesStats>( + "SELECT codeforces_handle FROM codeforces_stats WHERE member_id = $1", + ) + .bind(member.member_id) + .fetch_optional(pool.as_ref()) + .await; + + if let Ok(Some(codeforces_stats)) = codeforces_username { + let username = codeforces_stats.codeforces_handle.clone(); + + // Fetch and update Codeforces stats + match fetch_and_update_codeforces_stats(pool.clone(), member.member_id, &username).await + { + Ok(_) => println!("Codeforces stats updated for member ID: {}", member.member_id), + Err(e) => eprintln!( + "Failed to update Codeforces stats for member ID {}: {:?}", + member.member_id, e + ), + } + } + + // Update leaderboard + match update_leaderboard_scores(pool.clone()).await { + Ok(_) => println!("Leaderboard updated."), + Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), + } + } + } + Err(e) => eprintln!("Failed to fetch members: {:?}", e), + } +} + + + async fn update_attendance(members: Vec, pool: &PgPool) { #[allow(deprecated)] let today = chrono::Utc::now() @@ -199,4 +280,4 @@ async fn update_days_attended(member_id: i32, today: NaiveDate, pool: &PgPool) { ); } } -} +} \ No newline at end of file diff --git a/src/graphql/integration_test.rs b/src/graphql/integration_test.rs new file mode 100644 index 0000000..89c46d2 --- /dev/null +++ b/src/graphql/integration_test.rs @@ -0,0 +1,212 @@ +use config::{Config, File, FileFormat}; +use root::db::leaderboard::Leaderboard; +use root::db::member::Member; +use root::leaderboard::fetch_stats::{fetch_codeforces_stats, fetch_leetcode_stats}; +use root::leaderboard::update_leaderboard::update_leaderboard; +use sqlx::{postgres::PgPoolOptions, PgPool}; +use std::sync::Arc; + +pub fn get_database_url() -> String { + // Create a configuration instance to read Secrets.toml + let settings = Config::builder() + .add_source(File::new("Secrets", FileFormat::Toml)) + .build() + .expect("Failed to load Secrets.toml"); + + // Retrieve the `DATABASE_URL` from the file + settings + .get_string("DATABASE_URL") + .expect("Missing 'DATABASE_URL' in Secrets.toml") +} + +// Helper function to create a test database connection +async fn setup_test_db() -> PgPool { + let database_url = get_database_url(); + PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to create test database pool") +} + +// Helper function to clean up test data +async fn cleanup_test_data(pool: &PgPool) { + // sqlx::query("DELETE FROM leaderboard") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM leetcode_stats") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM codeforces_stats") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM Member") + // .execute(pool) + // .await + // .unwrap(); +} + +//test +#[tokio::test] +async fn test_insert_members_and_update_stats() { + let pool = setup_test_db().await; + + cleanup_test_data(&pool).await; + + // Define test members + let members = vec![ + ( + "B21CS1234", + "John Doe", + "Hostel A", + "john.doe@example.com", + "Male", + 2021, + "00:11:22:33:44:55", + Some("john_discord"), + "swayam-agrahari", + "tourist", + ), + ( + "B21CS5678", + "Jane Smith", + "Hostel B", + "jane.smith@example.com", + "Female", + 2021, + "66:77:88:99:AA:BB", + Some("jane_discord"), + "rihaan1810", + "tourist", + ), + ]; + + let mut inserted_members = Vec::new(); + + // Insert members and store their IDs + for member in &members { + // Insert Member + let member_result = sqlx::query_as::<_, Member>( + "INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, discord_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *", + ) + .bind(&member.0) + .bind(&member.1) + .bind(&member.2) + .bind(&member.3) + .bind(&member.4) + .bind(member.5) + .bind(&member.6) + .bind(&member.7) + .fetch_one(&pool) + .await + .expect("Failed to insert member"); + + // Insert LeetCode stats + let _leetcode_result = sqlx::query( + "INSERT INTO leetcode_stats (member_id, leetcode_username,problems_solved,easy_solved,medium_solved,hard_solved,contests_participated,best_rank,total_contests) + VALUES ($1, $2, 0,0,0,0,0,0,0)", + ) + .bind(member_result.id) + .bind(&member.8) + .execute(&pool) + .await + .expect("Failed to insert LeetCode stats"); + + // Insert Codeforces stats + let _codeforces_result = sqlx::query( + "INSERT INTO codeforces_stats (member_id, codeforces_handle,codeforces_rating,max_rating,contests_participated) + VALUES ($1, $2, 0,0,0)", + ) + .bind(member_result.id) + .bind(&member.9) + .execute(&pool) + .await + .expect("Failed to insert Codeforces stats"); + + inserted_members.push(member_result.id); + } + + // Test LeetCode stats fetching + for (member_id, leetcode_username) in inserted_members.iter().zip(members.iter().map(|m| m.8)) { + match fetch_leetcode_stats(Arc::new(pool.clone()), *member_id, leetcode_username).await { + Ok(_) => println!( + "Successfully fetched LeetCode stats for member ID: {}", + member_id + ), + Err(e) => { + println!("Error fetching LeetCode stats: {:?}", e); + // Uncomment to fail test on fetch error + // panic!("Failed to fetch LeetCode stats") + } + } + } + + // Test Codeforces stats fetching + for (member_id, codeforces_handle) in inserted_members.iter().zip(members.iter().map(|m| m.9)) { + match fetch_codeforces_stats(Arc::new(pool.clone()), *member_id, codeforces_handle).await { + Ok(_) => println!( + "Successfully fetched Codeforces stats for member ID: {}", + member_id + ), + Err(e) => { + println!("Error fetching Codeforces stats: {:?}", e); + // Uncomment to fail test on fetch error + // panic!("Failed to fetch Codeforces stats") + } + } + } + + // Test leaderboard update + match update_leaderboard(Arc::new(pool.clone())).await { + Ok(_) => println!("Successfully updated leaderboard"), + Err(e) => panic!("Failed to update leaderboard: {:?}", e), + } + + // Verify leaderboard entries + let leaderboard_entries = sqlx::query_as::<_, Leaderboard>("SELECT * FROM leaderboard") + .fetch_all(&pool) + .await + .unwrap(); + + assert_eq!( + leaderboard_entries.len(), + 2, + "Should have 2 leaderboard entries" + ); + + // Assertions about leaderboard scores + for entry in leaderboard_entries { + assert!(entry.unified_score > 0, "Unified score should be positive"); + assert!( + entry.leetcode_score.is_some(), + "LeetCode score should be set" + ); + assert!( + entry.codeforces_score.is_some(), + "Codeforces score should be set" + ); + } + + // Clean up + cleanup_test_data(&pool).await; +} + +// Additional helper test to verify database connections and basic operations +#[tokio::test] +async fn test_database_connection() { + let pool = setup_test_db().await; + let database_url = get_database_url(); + + // Print the URL to verify (optional, for debugging purposes) + println!("Database URL: {}", database_url); + + // Basic database connectivity test + let result = sqlx::query("SELECT 1").fetch_one(&pool).await; + + assert!(result.is_ok(), "Database connection and query should work"); +} \ No newline at end of file diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index 0d99324..d81397b 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,6 +1,6 @@ use async_graphql::MergedObject; -use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations}; -use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries}; +use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations,FetchLeetCode,FetchCodeForces,LeaderboardMutation}; +use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries, LeaderboardQueries}; pub mod mutations; pub mod queries; @@ -11,6 +11,7 @@ pub struct Query( AttendanceQueries, StreakQueries, ProjectQueries, + LeaderboardQueries, ); #[derive(MergedObject, Default)] @@ -19,4 +20,8 @@ pub struct Mutation( AttendanceMutations, StreakMutations, ProjectMutations, + FetchLeetCode, + FetchCodeForces, + LeaderboardMutation, + ); diff --git a/src/graphql/mutations/codeforces_status.rs b/src/graphql/mutations/codeforces_status.rs new file mode 100644 index 0000000..4c45c7e --- /dev/null +++ b/src/graphql/mutations/codeforces_status.rs @@ -0,0 +1,21 @@ +use async_graphql::{Context, Object, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use super::leaderboard_api::fetch_and_update_codeforces_stats; + +#[derive(Default)] +pub struct FetchCodeForces; + +#[Object] +impl FetchCodeForces { + pub async fn fetch_codeforces_stats( + &self, + ctx: &Context<'_>, + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; + fetch_and_update_codeforces_stats(pool.clone(), member_id, &username).await?; + Ok(true) + } +} diff --git a/src/graphql/mutations/leaderboard_api.rs b/src/graphql/mutations/leaderboard_api.rs new file mode 100644 index 0000000..1286762 --- /dev/null +++ b/src/graphql/mutations/leaderboard_api.rs @@ -0,0 +1,264 @@ +use reqwest; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; +use std::collections::HashMap; +use reqwest::Client; + + +pub async fn fetch_and_update_codeforces_stats( + pool: Arc, + member_id: i32, + username: &str, +) -> Result<(), Box>{ + let url = format!("https://codeforces.com/api/user.rating?handle={}", username); + let response = reqwest::get(&url).await?.text().await?; + let data: Value = serde_json::from_str(&response)?; + + if data["status"] == "OK" { + if let Some(results) = data["result"].as_array() { + let contests_participated = results.len() as i32; + + // Calculate the user's current and max ratings + let mut max_rating = 0; + let mut codeforces_rating = 0; + + for contest in results { + if let Some(new_rating) = contest["newRating"].as_i64() { + codeforces_rating = new_rating as i32; + max_rating = max_rating.max(codeforces_rating); + } + } + + let update_result = sqlx::query( + r#" + INSERT INTO codeforces_stats ( + member_id, codeforces_handle, codeforces_rating, max_rating, contests_participated + ) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (member_id) DO UPDATE SET + codeforces_handle = EXCLUDED.codeforces_handle, + codeforces_rating = EXCLUDED.codeforces_rating, + max_rating = EXCLUDED.max_rating, + contests_participated = EXCLUDED.contests_participated + "#, + ) + .bind(member_id) + .bind(username) + .bind(codeforces_rating) + .bind(max_rating) + .bind(contests_participated) + .execute(pool.as_ref()) + .await; + + match update_result { + Ok(_) => println!("Codeforces stats updated for member ID: {}", member_id), + Err(e) => eprintln!( + "Failed to update Codeforces stats for member ID {}: {:?}", + member_id, e + ), + } + + return Ok(()); + } + } + + Err(format!("Failed to fetch stats for Codeforces handle: {}", username).into()) +} + +pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Error> { + let leetcode_stats = sqlx::query!( + "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, + contests_participated, best_rank + FROM leetcode_stats" + ) + .fetch_all(pool.as_ref()) + .await?; + + let codeforces_stats = sqlx::query!( + "SELECT member_id, codeforces_rating, max_rating, contests_participated + FROM codeforces_stats" + ) + .fetch_all(pool.as_ref()) + .await?; + + let cf_lookup: HashMap = codeforces_stats + .iter() + .map(|row| { + ( + row.member_id, + (row.codeforces_rating, row.max_rating, row.contests_participated), + ) + }) + .collect(); + + for row in &leetcode_stats { + let leetcode_score = (5 * row.easy_solved) + + (10 * row.medium_solved) + + (20 * row.hard_solved) + + (2 * row.contests_participated) + + (100 - row.best_rank / 10).max(0); + + let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + .map(|(rating, max_rating, contests)| { + let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); + (cf_score, leetcode_score + cf_score) + }) + .unwrap_or((0, leetcode_score)); + + let result = sqlx::query!( + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (member_id) DO UPDATE SET + leetcode_score = EXCLUDED.leetcode_score, + codeforces_score = EXCLUDED.codeforces_score, + unified_score = EXCLUDED.unified_score, + last_updated = NOW()", + row.member_id, + leetcode_score, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + } + } + + for row in &codeforces_stats { + if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + continue; + } + + let codeforces_score = (row.codeforces_rating / 10) + + (row.max_rating / 20) + + (5 * row.contests_participated); + + let unified_score = codeforces_score; + + let result = sqlx::query!( + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (member_id) DO UPDATE SET + leetcode_score = EXCLUDED.leetcode_score, + codeforces_score = EXCLUDED.codeforces_score, + unified_score = EXCLUDED.unified_score, + last_updated = NOW()", + row.member_id, + 0, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + } + } + + Ok(()) +} + + + +pub async fn fetch_and_update_leetcode( + pool: Arc, + member_id: i32, + username: &str, +) -> Result<(), Box> { + let client = Client::new(); + let url = "https://leetcode.com/graphql"; + let query = r#" + query userProfile($username: String!) { + userContestRanking(username: $username) { + attendedContestsCount + } + matchedUser(username: $username) { + profile { + ranking + } + submitStats { + acSubmissionNum { + difficulty + count + } + } + } + } + "#; + + let response = client + .post(url) + .header("Content-Type", "application/json") + .json(&serde_json::json!({ + "query": query, + "variables": { "username": username } + })) + .send() + .await?; + + let data: Value = response.json().await?; + + let empty_vec = vec![]; + let submissions = data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"] + .as_array() + .unwrap_or(&empty_vec); + + let mut problems_solved = 0; + let mut easy_solved = 0; + let mut medium_solved = 0; + let mut hard_solved = 0; + + for stat in submissions { + let count = stat["count"].as_i64().unwrap_or(0) as i32; + match stat["difficulty"].as_str().unwrap_or("") { + "Easy" => easy_solved = count, + "Medium" => medium_solved = count, + "Hard" => hard_solved = count, + "All" => problems_solved = count, + _ => {} + } + } + + let contests_participated = data["data"]["userContestRanking"]["attendedContestsCount"] + .as_i64() + .unwrap_or(0) as i32; + let rank = data["data"]["matchedUser"]["profile"]["ranking"] + .as_i64() + .unwrap_or(0) as i32; + + sqlx::query!( + r#" + INSERT INTO leetcode_stats ( + member_id, leetcode_username, problems_solved, easy_solved, medium_solved, + hard_solved, contests_participated, best_rank, total_contests + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (member_id) DO UPDATE SET + leetcode_username = EXCLUDED.leetcode_username, + problems_solved = EXCLUDED.problems_solved, + easy_solved = EXCLUDED.easy_solved, + medium_solved = EXCLUDED.medium_solved, + hard_solved = EXCLUDED.hard_solved, + contests_participated = EXCLUDED.contests_participated, + best_rank = EXCLUDED.best_rank, + total_contests = EXCLUDED.total_contests + "#, + member_id, + username, + problems_solved, + easy_solved, + medium_solved, + hard_solved, + contests_participated, + rank, + contests_participated + ) + .execute(pool.as_ref()) + .await?; + + Ok(()) +} diff --git a/src/graphql/mutations/leaderboard_mutation.rs b/src/graphql/mutations/leaderboard_mutation.rs new file mode 100644 index 0000000..52fa7d9 --- /dev/null +++ b/src/graphql/mutations/leaderboard_mutation.rs @@ -0,0 +1,56 @@ +use async_graphql::{Context, Object}; +use sqlx::PgPool; +use std::sync::Arc; + +use crate::db::leaderboard::{CodeforcesStats, LeetCodeStats}; + +pub struct LeadMutation; + +#[Object] +impl LeadMutation { + pub async fn add_or_update_leetcode_username( + &self, + ctx: &Context<'_>, + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; + + sqlx::query_as::<_, LeetCodeStats>( + " + INSERT INTO leetcode_stats (member_id, leetcode_username, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank, total_contests) + VALUES ($1, $2, 0, 0, 0, 0, 0, 0, 0) + ON CONFLICT (member_id) DO UPDATE + SET leetcode_username = EXCLUDED.leetcode_username + RETURNING * + ", + ) + .bind(member_id) + .bind(username) + .fetch_one(pool.as_ref()) + .await + } + + async fn add_or_update_codeforces_handle( + &self, + ctx: &Context<'_>, + member_id: i32, + handle: String, + ) -> Result { + let pool = ctx.data::>()?; + + sqlx::query_as::<_, CodeforcesStats>( + " + INSERT INTO codeforces_stats (member_id, codeforces_handle, codeforces_rating, max_rating, contests_participated) + VALUES ($1, $2, 0, 0, 0) + ON CONFLICT (member_id) DO UPDATE + SET codeforces_handle = EXCLUDED.codeforces_handle + RETURNING * + ", + ) + .bind(member_id) + .bind(handle) + .fetch_one(pool.as_ref()) + .await + } +} diff --git a/src/graphql/mutations/leetcode_status.rs b/src/graphql/mutations/leetcode_status.rs new file mode 100644 index 0000000..6c7e69b --- /dev/null +++ b/src/graphql/mutations/leetcode_status.rs @@ -0,0 +1,21 @@ +use async_graphql::{Context, Object, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use super::leaderboard_api::fetch_and_update_leetcode; + +#[derive(Default)] +pub struct FetchLeetCode; + +#[Object] +impl FetchLeetCode { + pub async fn fetch_leetcode_stats( + &self, + ctx: &Context<'_>, + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; + fetch_and_update_leetcode(pool.clone(), member_id, &username).await?; + Ok(true) + } +} diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index 012ed2a..2bbe32a 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -2,8 +2,22 @@ pub mod attendance_mutations; pub mod member_mutations; pub mod project_mutations; pub mod streak_mutations; +pub mod update_leaderboard; //leaderboard +pub mod leetcode_status; +pub mod codeforces_status; +pub mod leaderboard_api; + pub use attendance_mutations::AttendanceMutations; pub use member_mutations::MemberMutations; pub use project_mutations::ProjectMutations; pub use streak_mutations::StreakMutations; +pub use leetcode_status::FetchLeetCode; +pub use codeforces_status::FetchCodeForces; +pub use update_leaderboard::LeaderboardMutation; +pub use leaderboard_api::fetch_and_update_codeforces_stats; +pub use leaderboard_api::fetch_and_update_leetcode; +pub use leaderboard_api::update_leaderboard_scores; + + +//use any mutations for leaderboard if needed \ No newline at end of file diff --git a/src/graphql/mutations/update_leaderboard.rs b/src/graphql/mutations/update_leaderboard.rs new file mode 100644 index 0000000..3e57c0b --- /dev/null +++ b/src/graphql/mutations/update_leaderboard.rs @@ -0,0 +1,113 @@ +use async_graphql::{Context, Object, Result as GqlResult}; +use sqlx::{PgPool}; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Default)] +pub struct LeaderboardMutation; + +#[Object] +impl LeaderboardMutation { + pub async fn update_leaderboard(&self, ctx: &Context<'_>) -> GqlResult { + let pool = ctx.data::>() + .map_err(|_| async_graphql::Error::new("Failed to access the database pool"))?; + + + let leetcode_stats = sqlx::query!( + "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, + contests_participated, best_rank + FROM leetcode_stats" + ) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {:?}", e)))?; + + + let codeforces_stats = sqlx::query!( + "SELECT member_id, codeforces_rating, max_rating, contests_participated + FROM codeforces_stats" + ) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {:?}", e)))?; + + let cf_lookup: HashMap = codeforces_stats + .iter() + .map(|row| { + ( + row.member_id, + (row.codeforces_rating, row.max_rating, row.contests_participated), + ) + }) + .collect(); + + for row in &leetcode_stats { + let leetcode_score = (5 * row.easy_solved) + + (10 * row.medium_solved) + + (20 * row.hard_solved) + + (2 * row.contests_participated) + + (100 - row.best_rank / 10).max(0); + + let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + .map(|(rating, max_rating, contests)| { + let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); + (cf_score, leetcode_score + cf_score) + }) + .unwrap_or((0, leetcode_score)); + + let result = sqlx::query!( + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (member_id) DO UPDATE SET + leetcode_score = EXCLUDED.leetcode_score, + codeforces_score = EXCLUDED.codeforces_score, + unified_score = EXCLUDED.unified_score, + last_updated = NOW()", + row.member_id, + leetcode_score, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + } + } + + for row in &codeforces_stats { + if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + continue; + } + + let codeforces_score = (row.codeforces_rating / 10) + + (row.max_rating / 20) + + (5 * row.contests_participated); + + let unified_score = codeforces_score; + + let result = sqlx::query!( + "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (member_id) DO UPDATE SET + leetcode_score = EXCLUDED.leetcode_score, + codeforces_score = EXCLUDED.codeforces_score, + unified_score = EXCLUDED.unified_score, + last_updated = NOW()", + row.member_id, + 0, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + } + } + + Ok(true) + } +} diff --git a/src/graphql/queries/leaderboard_queries.rs b/src/graphql/queries/leaderboard_queries.rs new file mode 100644 index 0000000..ff25ff8 --- /dev/null +++ b/src/graphql/queries/leaderboard_queries.rs @@ -0,0 +1,65 @@ +use async_graphql::{Context, Object}; +use sqlx::PgPool; +use std::sync::Arc; + +use crate::models::leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}; + +#[derive(Default)] +pub struct LeaderboardQueries; + +#[Object] +impl LeaderboardQueries { + async fn get_unified_leaderboard( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let leaderboard = sqlx::query_as::<_, LeaderboardWithMember>( + "SELECT l.*, m.name AS member_name + FROM leaderboard l + JOIN member m ON l.member_id = m.member_id + ORDER BY unified_score DESC", + ) + .fetch_all(pool.as_ref()) + .await?; + Ok(leaderboard) + } + + async fn get_leetcode_stats( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let leetcode_stats = sqlx::query_as::<_, LeetCodeStatsWithName>( + "SELECT l.*, m.name AS member_name + FROM leetcode_stats l + JOIN member m ON l.member_id = m.member_id + ORDER BY best_rank", + ) + .fetch_all(pool.as_ref()) + .await?; + Ok(leetcode_stats) + } + + async fn get_codeforces_stats( + &self, + ctx: &Context<'_>, + ) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let codeforces_stats = sqlx::query_as::<_, CodeforcesStatsWithName>( + "SELECT c.*, m.name AS member_name + FROM codeforces_stats c + JOIN member m ON c.member_id = m.member_id + ORDER BY max_rating DESC", + ) + .fetch_all(pool.as_ref()) + .await?; + Ok(codeforces_stats) + } +} diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs index 49a8263..e533c1c 100644 --- a/src/graphql/queries/mod.rs +++ b/src/graphql/queries/mod.rs @@ -2,8 +2,11 @@ pub mod attendance_queries; pub mod member_queries; pub mod project_queries; pub mod streak_queries; +pub mod leaderboard_queries; + pub use attendance_queries::AttendanceQueries; pub use member_queries::MemberQueries; pub use project_queries::ProjectQueries; pub use streak_queries::StreakQueries; +pub use leaderboard_queries::LeaderboardQueries; diff --git a/src/models/leaderboard.rs b/src/models/leaderboard.rs new file mode 100644 index 0000000..7f9d915 --- /dev/null +++ b/src/models/leaderboard.rs @@ -0,0 +1,71 @@ +use async_graphql::SimpleObject; +use sqlx::FromRow; + +#[derive(FromRow, SimpleObject)] +pub struct Leaderboard { + pub id: i32, + pub member_id: i32, + pub leetcode_score: Option, + pub codeforces_score: Option, + pub unified_score: i32, + pub last_updated: Option, +} + +#[derive(FromRow, SimpleObject)] +pub struct LeaderboardWithMember { + pub id: i32, + pub member_id: i32, + pub member_name: String, + pub leetcode_score: Option, + pub codeforces_score: Option, + pub unified_score: i32, + pub last_updated: Option, +} + +#[derive(FromRow, SimpleObject)] +pub struct LeetCodeStats { + pub id: i32, + pub member_id: i32, + pub leetcode_username: String, + pub problems_solved: i32, + pub easy_solved: i32, + pub medium_solved: i32, + pub hard_solved: i32, + pub contests_participated: i32, + pub best_rank: i32, + pub total_contests: i32, +} + +#[derive(FromRow, SimpleObject)] +pub struct LeetCodeStatsWithName { + pub id: i32, + pub member_id: i32, + pub member_name: String, + pub leetcode_username: String, + pub problems_solved: i32, + pub easy_solved: i32, + pub medium_solved: i32, + pub hard_solved: i32, + pub contests_participated: i32, + pub best_rank: i32, + pub total_contests: i32, +} + +#[derive(FromRow, SimpleObject)] +pub struct CodeforcesStats { + pub member_id: i32, + pub codeforces_handle: String, + pub codeforces_rating: i32, + pub max_rating: i32, + pub contests_participated: i32, +} + +#[derive(FromRow, SimpleObject)] +pub struct CodeforcesStatsWithName { + pub member_id: i32, + pub member_name: String, + pub codeforces_handle: String, + pub codeforces_rating: i32, + pub max_rating: i32, + pub contests_participated: i32, +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index fbc7dac..e8c6d86 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod attendance; +pub mod leaderboard; pub mod member; pub mod project; pub mod status_update_streak;