From 6da58bbd65b829ac826ff1da3ab2ac2d70c283cb Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 22 Jun 2024 22:14:27 -0700 Subject: [PATCH 001/168] upgrade typescript, add partykit --- package.json | 8 +- yarn.lock | 735 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 661 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index eacb69b5a..5b7937f3f 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "normalize.css": "^8.0.1", "p-queue": "^8.0.1", "papaparse": "^5.4.1", + "partykit": "0.0.107", "peerjs": "^2.0.0-beta.3", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", @@ -105,7 +106,7 @@ "sanitize-filename": "^1.6.3", "simfile-parser": "^0.7.2", "style-loader": "^4.0.0", - "typescript": "^5.4.5", + "typescript": "^5.5.2", "undici": "^6.19.0", "victory": "^37.0.2", "webpack": "^5.92.0", @@ -121,5 +122,8 @@ "semver": "^7.5.2", "xml2js": "^0.5.0" }, - "packageManager": "yarn@4.3.0" + "packageManager": "yarn@4.3.0", + "dependencies": { + "partysocket": "1.0.1" + } } diff --git a/yarn.lock b/yarn.lock index 5a9323061..addecec64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,14 +43,7 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.24.6": - version: 7.24.6 - resolution: "@babel/compat-data@npm:7.24.6" - checksum: 10/c355141e4649ef6efa413d71cfc1efb183be46b8fc945fc17e3c7f4313b4b566af575a4183450697916cd6b8c7f180e315986b5d7f07e7b7afd0786594754f7d - languageName: node - linkType: hard - -"@babel/compat-data@npm:^7.24.7": +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.24.7": version: 7.24.7 resolution: "@babel/compat-data@npm:7.24.7" checksum: 10/6edc09152ca51a22c33741c441f33f9475598fa59edc53369edb74b49f4ea4bef1281f5b0ed2b9b67fb66faef2da2069e21c4eef83405d8326e524b301f4e7e2 @@ -92,7 +85,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.24.6, @babel/helper-annotate-as-pure@npm:^7.24.7": +"@babel/helper-annotate-as-pure@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" dependencies: @@ -111,20 +104,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.6": - version: 7.24.6 - resolution: "@babel/helper-compilation-targets@npm:7.24.6" - dependencies: - "@babel/compat-data": "npm:^7.24.6" - "@babel/helper-validator-option": "npm:^7.24.6" - browserslist: "npm:^4.22.2" - lru-cache: "npm:^5.1.1" - semver: "npm:^6.3.1" - checksum: 10/28f34f2c9e0ec047360c4dca8d4fb99009e868f9c1acad0ca125f2f9990790897216155d44935209c6e4c4e0318f5a9a46304771d75823add7400e3079945314 - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.24.7": +"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-compilation-targets@npm:7.24.7" dependencies: @@ -156,20 +136,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6": - version: 7.24.6 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.24.6" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.24.6" - regexpu-core: "npm:^5.3.1" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/74e717c71d7c007cc81537566c70b28ac75403afb499db2b1b988904dcda0a09a958c4c4b7d74821d0932e73f1c56227f6371ed751b16ae679aa8a2e4a271d64 - languageName: node - linkType: hard - -"@babel/helper-create-regexp-features-plugin@npm:^7.24.7": +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.24.7" dependencies: @@ -345,13 +312,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.24.6": - version: 7.24.6 - resolution: "@babel/helper-validator-option@npm:7.24.6" - checksum: 10/5defb2da74e1cac9497016f4e41698aeed75ec7a5e9dc07e777cdb67ef73cd2e27bd2bf8a3ab8d37e0b93a6a45524a9728f03e263afdef452436cf74794bde87 - languageName: node - linkType: hard - "@babel/helper-validator-option@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-option@npm:7.24.7" @@ -1595,6 +1555,57 @@ __metadata: languageName: node linkType: hard +"@cloudflare/workerd-darwin-64@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "@cloudflare/workerd-darwin-64@npm:1.20240610.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@cloudflare/workerd-darwin-arm64@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20240610.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@cloudflare/workerd-linux-64@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "@cloudflare/workerd-linux-64@npm:1.20240610.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@cloudflare/workerd-linux-arm64@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "@cloudflare/workerd-linux-arm64@npm:1.20240610.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@cloudflare/workerd-windows-64@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "@cloudflare/workerd-windows-64@npm:1.20240610.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@cloudflare/workers-types@npm:4.20240614.0": + version: 4.20240614.0 + resolution: "@cloudflare/workers-types@npm:4.20240614.0" + checksum: 10/81e2f6c577a820f9f00a673eb9465afed6b9969c3e3c3d69861342fd175b61d30b499edfbb65050122deeda576f92e13606174893eaea3b3544d13363d5a40be + languageName: node + linkType: hard + +"@cspotcode/source-map-support@npm:0.8.1": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10/b6e38a1712fab242c86a241c229cf562195aad985d0564bd352ac404be583029e89e93028ffd2c251d2c407ecac5fb0cbdca94a2d5c10f29ac806ede0508b3ff + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -1611,6 +1622,167 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -1653,6 +1825,13 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 10/2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 + languageName: node + linkType: hard + "@formatjs/ecma402-abstract@npm:2.0.0": version: 2.0.0 resolution: "@formatjs/ecma402-abstract@npm:2.0.0" @@ -2390,10 +2569,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.1 - resolution: "@jridgewell/resolve-uri@npm:3.1.1" - checksum: 10/64d59df8ae1a4e74315eb1b61e012f1c7bc8aac47a3a1e683f6fe7008eab07bc512a742b7aa7c0405685d1421206de58c9c2e6adbfe23832f8bd69408ffc183e +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10/97106439d750a409c22c8bff822d648f6a71f3aa9bc8e5129efdc36343cd3096ddc4eeb1c62d2fe48e9bdd4db37b05d4646a17114ecebd3bbcacfa2de51c3c1d languageName: node linkType: hard @@ -2421,6 +2600,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10/83deafb8e7a5ca98993c2c6eeaa93c270f6f647a4c0dc00deb38c9cf9b2d3b7bf15e8839540155247ef034a052c0ec4466f980bf0c9e2ab63b97d16c0cedd3ff + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -3410,12 +3599,21 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.11.3 - resolution: "acorn@npm:8.11.3" +"acorn-walk@npm:^8.2.0": + version: 8.3.3 + resolution: "acorn-walk@npm:8.3.3" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10/59701dcb7070679622ba8e9c7f37577b4935565747ca0fd7c1c3ad30b3f1b1b008276282664e323b5495eb49f77fa12d3816fd06dc68e18f90fbebe759f71450 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.12.0 + resolution: "acorn@npm:8.12.0" bin: acorn: bin/acorn - checksum: 10/b688e7e3c64d9bfb17b596e1b35e4da9d50553713b3b3630cf5690f2b023a84eac90c56851e6912b483fe60e8b4ea28b254c07e92f17ef83d72d78745a8352dd + checksum: 10/550cc5033184eb98f7fbe2e9ddadd0f47f065734cc682f25db7a244f52314eb816801b64dec7174effd978045bd1754892731a90b1102b0ede9d17a15cfde138 languageName: node linkType: hard @@ -3623,6 +3821,15 @@ __metadata: languageName: node linkType: hard +"as-table@npm:^1.0.36": + version: 1.0.55 + resolution: "as-table@npm:1.0.55" + dependencies: + printable-characters: "npm:^1.0.42" + checksum: 10/8bbfbd7b6f240efb22f6553f756e89d1cae074e9f7a24580282e9d247c1bd9cf1fd9fb49056202a78a5e69907209d8bf032d8b6c3eaaab5fb6ad92da64a7894a + languageName: node + linkType: hard + "async@npm:^3.2.3": version: 3.2.5 resolution: "async@npm:3.2.5" @@ -4015,6 +4222,16 @@ __metadata: languageName: node linkType: hard +"capnp-ts@npm:^0.7.0": + version: 0.7.0 + resolution: "capnp-ts@npm:0.7.0" + dependencies: + debug: "npm:^4.3.1" + tslib: "npm:^2.2.0" + checksum: 10/186a76662e31ab16fe46fe0785ed2a511969d0c5198e2d7baec6b44f71c9b3bf8c05e7627036dc86c2d3ddc229c846559350c13f904fdd8da3590d7054715ba8 + languageName: node + linkType: hard + "chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -4187,6 +4404,17 @@ __metadata: languageName: node linkType: hard +"clipboardy@npm:4.0.0": + version: 4.0.0 + resolution: "clipboardy@npm:4.0.0" + dependencies: + execa: "npm:^8.0.1" + is-wsl: "npm:^3.1.0" + is64bit: "npm:^2.0.0" + checksum: 10/ec4ebe7e5c81d9c9cb994637e7b0e068c1c8fc272167ecd5519f967427271ec66e0e64da7268a2630b860eff42933aeabe25ba5e42bb80dbf1fae6362df059ed + languageName: node + linkType: hard + "clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" @@ -4401,6 +4629,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: 10/aae7911ddc5f444a9025fbd979ad1b5d60191011339bce48e555cb83343d0f98b865ff5c4d71fecdfb8555a5cafdc65632f6fce172f32aaf6936830a883a0380 + languageName: node + linkType: hard + "copy-webpack-plugin@npm:^12.0.2": version: 12.0.2 resolution: "copy-webpack-plugin@npm:12.0.2" @@ -4805,6 +5040,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^2.0.0": + version: 2.0.2 + resolution: "data-uri-to-buffer@npm:2.0.2" + checksum: 10/152bec5e77513ee253a7c686700a1723246f582dad8b614e8eaaaba7fa45a15c8671ae4b8f4843f4f3a002dae1d3e7a20f852f7d7bdc8b4c15cfe7adfdfb07f8 + languageName: node + linkType: hard + "data-uri-to-buffer@npm:^4.0.0": version: 4.0.1 resolution: "data-uri-to-buffer@npm:4.0.1" @@ -4904,6 +5146,8 @@ __metadata: normalize.css: "npm:^8.0.1" p-queue: "npm:^8.0.1" papaparse: "npm:^5.4.1" + partykit: "npm:0.0.107" + partysocket: "npm:1.0.1" peerjs: "npm:^2.0.0-beta.3" postcss: "npm:^8.4.38" postcss-loader: "npm:^8.1.1" @@ -4916,7 +5160,7 @@ __metadata: sanitize-filename: "npm:^1.6.3" simfile-parser: "npm:^0.7.2" style-loader: "npm:^4.0.0" - typescript: "npm:^5.4.5" + typescript: "npm:^5.5.2" undici: "npm:^6.19.0" victory: "npm:^37.0.2" webpack: "npm:^5.92.0" @@ -5463,6 +5707,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:0.21.5": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -5685,6 +6009,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^6.0.2": + version: 6.0.2 + resolution: "event-target-shim@npm:6.0.2" + checksum: 10/aa69fc4193cad3f1e4dc0c2d3f2689ea2d477f5ff2fbee8b65f866035b15658e1985932b06ba2190c3d2cc9cc6802c26facd6c60487590c1a05f44545ec24f42 + languageName: node + linkType: hard + "eventemitter3@npm:^3.1.2": version: 3.1.2 resolution: "eventemitter3@npm:3.1.2" @@ -5730,6 +6061,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d + languageName: node + linkType: hard + "exif-parser@npm:^0.1.12": version: 0.1.12 resolution: "exif-parser@npm:0.1.12" @@ -5737,6 +6085,13 @@ __metadata: languageName: node linkType: hard +"exit-hook@npm:^2.2.1": + version: 2.2.1 + resolution: "exit-hook@npm:2.2.1" + checksum: 10/75835919e0aca624daa8d114c0014ae84506c4b79ac5806748cc7a86d1610a864ee974be58eec823c7757e5e6b07a5e332647e20ef84f6cc3dc3385c953c78c9 + languageName: node + linkType: hard + "expand-template@npm:^2.0.3": version: 2.0.3 resolution: "expand-template@npm:2.0.3" @@ -6183,7 +6538,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.2": +"fsevents@npm:2.3.3, fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -6193,7 +6548,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -6236,6 +6591,16 @@ __metadata: languageName: node linkType: hard +"get-source@npm:^2.0.12": + version: 2.0.12 + resolution: "get-source@npm:2.0.12" + dependencies: + data-uri-to-buffer: "npm:^2.0.0" + source-map: "npm:^0.6.1" + checksum: 10/6ba35ae0755046199b57d7fe254d50c6d7550d3b150e065a3607e3da8c55c617302f4c7cc3712252c7810954a04e2e56467ad02a0798c0841a5e980064bd3048 + languageName: node + linkType: hard + "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -6243,6 +6608,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 10/dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e + languageName: node + linkType: hard + "gifwrap@npm:^0.10.1": version: 0.10.1 resolution: "gifwrap@npm:0.10.1" @@ -6694,6 +7066,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 10/30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -7060,6 +7439,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 10/172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 + languageName: node + linkType: hard + "is-unicode-supported@npm:^0.1.0": version: 0.1.0 resolution: "is-unicode-supported@npm:0.1.0" @@ -7076,6 +7462,15 @@ __metadata: languageName: node linkType: hard +"is64bit@npm:^2.0.0": + version: 2.0.0 + resolution: "is64bit@npm:2.0.0" + dependencies: + system-architecture: "npm:^0.1.0" + checksum: 10/94dafd5f29bfb96c542e89ef8c33e811159ca7d07a2890ab83026fa87706612af4101308d9392e9ee68e046e8604a6b59a8f41091f8556f6235efbcfd9c5574c + languageName: node + linkType: hard + "isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -7739,6 +8134,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10/995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 + languageName: node + linkType: hard + "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -7767,6 +8169,28 @@ __metadata: languageName: node linkType: hard +"miniflare@npm:3.20240610.0": + version: 3.20240610.0 + resolution: "miniflare@npm:3.20240610.0" + dependencies: + "@cspotcode/source-map-support": "npm:0.8.1" + acorn: "npm:^8.8.0" + acorn-walk: "npm:^8.2.0" + capnp-ts: "npm:^0.7.0" + exit-hook: "npm:^2.2.1" + glob-to-regexp: "npm:^0.4.1" + stoppable: "npm:^1.1.0" + undici: "npm:^5.28.2" + workerd: "npm:1.20240610.1" + ws: "npm:^8.11.0" + youch: "npm:^3.2.2" + zod: "npm:^3.20.6" + bin: + miniflare: bootstrap.js + checksum: 10/0febb70dbcddd90059d0f57993f357cab5be9dec31a5f61010561b9f625a203cbc0bdfc4eaead26f9e26fcaba77aeef08f39b877e44ee407aaf6114e1368dda9 + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -7957,6 +8381,15 @@ __metadata: languageName: node linkType: hard +"mustache@npm:^4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 10/6e668bd5803255ab0779c3983b9412b5c4f4f90e822230e0e8f414f5449ed7a137eed29430e835aa689886f663385cfe05f808eb34b16e1f3a95525889b05cd3 + languageName: node + linkType: hard + "mute-stream@npm:1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" @@ -8161,6 +8594,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 10/ae8e7a89da9594fb9c308f6555c73f618152340dcaae423e5fb3620026fefbec463618a8b761920382d666fa7a2d8d240b6fe320e8a6cdd54dc3687e2b659d25 + languageName: node + linkType: hard + "nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" @@ -8239,6 +8681,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 10/0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 + languageName: node + linkType: hard + "open@npm:^10.0.3": version: 10.0.3 resolution: "open@npm:10.0.3" @@ -8495,6 +8946,34 @@ __metadata: languageName: node linkType: hard +"partykit@npm:0.0.107": + version: 0.0.107 + resolution: "partykit@npm:0.0.107" + dependencies: + "@cloudflare/workers-types": "npm:4.20240614.0" + clipboardy: "npm:4.0.0" + esbuild: "npm:0.21.5" + fsevents: "npm:2.3.3" + miniflare: "npm:3.20240610.0" + yoga-wasm-web: "npm:0.3.3" + dependenciesMeta: + fsevents: + optional: true + bin: + partykit: dist/bin.mjs + checksum: 10/66054b0f5ccbe6d5c4b2ac0b1af3994bc2a81d621d24553e535d53093babed352e413fa7d4696676f40b57d23051bce764652b21a1825e864b5a77576a06f635 + languageName: node + linkType: hard + +"partysocket@npm:1.0.1": + version: 1.0.1 + resolution: "partysocket@npm:1.0.1" + dependencies: + event-target-shim: "npm:^6.0.2" + checksum: 10/07120a825a2006c54b4c3bd15a57372d35050c393f5f6ae64c779d6c73e349a2d85501d965dab6640673400a40fb34d82d08dc0585f824a31095e6051485ef21 + languageName: node + linkType: hard + "pascal-case@npm:^3.1.2": version: 3.1.2 resolution: "pascal-case@npm:3.1.2" @@ -8550,6 +9029,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -9140,16 +9626,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.2.5": - version: 3.3.0 - resolution: "prettier@npm:3.3.0" - bin: - prettier: bin/prettier.cjs - checksum: 10/e55233f8e4b5f96f52180dbfa424ae797a98a9b8a9a7a79de5004e522c02b423e71927ed99d855dbfcd00dc3b82e5f6fb304cfe117cc4e7c8477d883df2d8984 - languageName: node - linkType: hard - -"prettier@npm:^3.3.2": +"prettier@npm:^3.2.5, prettier@npm:^3.3.2": version: 3.3.2 resolution: "prettier@npm:3.3.2" bin: @@ -9168,6 +9645,13 @@ __metadata: languageName: node linkType: hard +"printable-characters@npm:^1.0.42": + version: 1.0.42 + resolution: "printable-characters@npm:1.0.42" + checksum: 10/5fd9f44f2b24c9d875a97642a72be27f53aaac7f0f8f2792f969f3082e4516878db21cfa999f827606b002a890e6afeac0e0cc8dcb0d2d7965252975e634c6b2 + languageName: node + linkType: hard + "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" @@ -10099,7 +10583,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -10229,7 +10713,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:~0.6.0": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff @@ -10286,6 +10770,16 @@ __metadata: languageName: node linkType: hard +"stacktracey@npm:^2.1.8": + version: 2.1.8 + resolution: "stacktracey@npm:2.1.8" + dependencies: + as-table: "npm:^1.0.36" + get-source: "npm:^2.0.12" + checksum: 10/c87f708b639636788c4b46ecc6e503c27b6124bec724bcdc3180d7cdddfab0dee370225009e3b407adaedf847362cfc77af64f01c805516e39a28d16c6d40df8 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -10300,6 +10794,13 @@ __metadata: languageName: node linkType: hard +"stoppable@npm:^1.1.0": + version: 1.1.0 + resolution: "stoppable@npm:1.1.0" + checksum: 10/63104fcbdece130bc4906fd982061e763d2ef48065ed1ab29895e5ad00552c625f8a4c50c9cd2e3bfa805c8a2c3bfdda0f07c5ae39694bd2d5cb0bee1618d1e9 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -10365,6 +10866,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 10/23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -10478,6 +10986,13 @@ __metadata: languageName: node linkType: hard +"system-architecture@npm:^0.1.0": + version: 0.1.0 + resolution: "system-architecture@npm:0.1.0" + checksum: 10/ca0dd793c45c354ab57dd7fc8ce7dc9923a6e07382bd3b22eb5b08f55ddb0217c390d00767549c5155fd4ce7ef23ffdd8cfb33dd4344cbbd37837d085a50f6f0 + languageName: node + linkType: hard + "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" @@ -10704,10 +11219,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:~2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:~2.6.2": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c languageName: node linkType: hard @@ -10767,23 +11282,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.4.5": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" +"typescript@npm:^5.5.2": + version: 5.5.2 + resolution: "typescript@npm:5.5.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/d04a9e27e6d83861f2126665aa8d84847e8ebabcea9125b9ebc30370b98cb38b5dff2508d74e2326a744938191a83a69aa9fddab41f193ffa43eabfdf3f190a5 + checksum: 10/9118b20f248e76b0dbff8737fef65dfa89d02668d4e633d2c5ceac99033a0ca5e8a1c1a53bc94da68e8f67677a88f318663dde859c9e9a09c1e116415daec2ba languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.5#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.5.2#optional!builtin": + version: 5.5.2 + resolution: "typescript@patch:typescript@npm%3A5.5.2#optional!builtin::version=5.5.2&hash=b45daf" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/760f7d92fb383dbf7dee2443bf902f4365db2117f96f875cf809167f6103d55064de973db9f78fe8f31ec08fff52b2c969aee0d310939c0a3798ec75d0bca2e1 + checksum: 10/28b3de2ddaf63a7620e7ddbe5d377af71ce93ecc558c41bf0e3d88661d8e6e7aa6c7739164fef98055f69819e41faca49252938ef3633a3dff2734cca6a9042e languageName: node linkType: hard @@ -10794,6 +11309,15 @@ __metadata: languageName: node linkType: hard +"undici@npm:^5.28.2": + version: 5.28.4 + resolution: "undici@npm:5.28.4" + dependencies: + "@fastify/busboy": "npm:^2.0.0" + checksum: 10/a666a9f5ac4270c659fafc33d78b6b5039a0adbae3e28f934774c85dcc66ea91da907896f12b414bd6f578508b44d5dc206fa636afa0e49a4e1c9e99831ff065 + languageName: node + linkType: hard + "undici@npm:^6.19.0": version: 6.19.0 resolution: "undici@npm:6.19.0" @@ -11715,6 +12239,32 @@ __metadata: languageName: node linkType: hard +"workerd@npm:1.20240610.1": + version: 1.20240610.1 + resolution: "workerd@npm:1.20240610.1" + dependencies: + "@cloudflare/workerd-darwin-64": "npm:1.20240610.1" + "@cloudflare/workerd-darwin-arm64": "npm:1.20240610.1" + "@cloudflare/workerd-linux-64": "npm:1.20240610.1" + "@cloudflare/workerd-linux-arm64": "npm:1.20240610.1" + "@cloudflare/workerd-windows-64": "npm:1.20240610.1" + dependenciesMeta: + "@cloudflare/workerd-darwin-64": + optional: true + "@cloudflare/workerd-darwin-arm64": + optional: true + "@cloudflare/workerd-linux-64": + optional: true + "@cloudflare/workerd-linux-arm64": + optional: true + "@cloudflare/workerd-windows-64": + optional: true + bin: + workerd: bin/workerd + checksum: 10/c37cd30c25fbdc7f97a296cf4b1a8ad2dbb65621a9c4b4bb24cb8fc00d9674c6610c5d2e5dc87c6cda83aa7b3b23a63d84b924ec1d9a3881622a9284590723f6 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -11755,7 +12305,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.16.0, ws@npm:^8.17.0": +"ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.0": version: 8.17.1 resolution: "ws@npm:8.17.1" peerDependencies: @@ -11864,6 +12414,24 @@ __metadata: languageName: node linkType: hard +"yoga-wasm-web@npm:0.3.3": + version: 0.3.3 + resolution: "yoga-wasm-web@npm:0.3.3" + checksum: 10/7a60a7dbb6dc0383395f2b40c9fac6e4f7b322f934ebb07cefc92997baca597f3ef01f67fe37e5e61c128de917760aa7639980ed2f8d94c2dfbef83bd5f61e36 + languageName: node + linkType: hard + +"youch@npm:^3.2.2": + version: 3.3.3 + resolution: "youch@npm:3.3.3" + dependencies: + cookie: "npm:^0.5.0" + mustache: "npm:^4.2.0" + stacktracey: "npm:^2.1.8" + checksum: 10/d13fb6f1e756823397ba02dd0c6f8a1b060a21327238872b46677ea1e1570b6b90fe216560a2ff9f34ba2eead6c442403b70df44354c6224bbfe31cc9a182c38 + languageName: node + linkType: hard + "zip-webpack-plugin@npm:^4.0.0": version: 4.0.1 resolution: "zip-webpack-plugin@npm:4.0.1" @@ -11876,6 +12444,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.20.6": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 + languageName: node + linkType: hard + "zustand@npm:^4.3.9": version: 4.4.7 resolution: "zustand@npm:4.4.7" From 6636fe7c4ed8b638f67612f2f64ec986375b67c9 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 22 Jun 2024 22:18:25 -0700 Subject: [PATCH 002/168] initial working-ish prototype --- .gitignore | 1 + partykit.json | 6 + src/config-persistence.ts | 55 ++++- src/config-state.ts | 9 +- src/controls/controls-drawer.tsx | 24 +- src/draw-state.tsx | 28 +-- src/drawn-set.tsx | 2 - src/models/Drawing.ts | 3 - src/party/README.md | 1 + src/party/client.ts | 40 ++++ src/party/server.ts | 77 ++++++ src/party/types.ts | 31 +++ src/tournament-mode/drawing-actions.tsx | 73 +----- src/tournament-mode/remote-peer-menu.tsx | 146 ------------ src/tournament-mode/remote-peers.ts | 288 ----------------------- src/tournament-mode/sync-with-peers.ts | 26 -- src/zustand/contextual-zustand.ts | 10 +- 17 files changed, 223 insertions(+), 597 deletions(-) create mode 100644 partykit.json create mode 100644 src/party/README.md create mode 100644 src/party/client.ts create mode 100644 src/party/server.ts create mode 100644 src/party/types.ts delete mode 100644 src/tournament-mode/remote-peer-menu.tsx delete mode 100644 src/tournament-mode/remote-peers.ts delete mode 100644 src/tournament-mode/sync-with-peers.ts diff --git a/.gitignore b/.gitignore index 9eb97107e..6210c7177 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !.yarn/releases !.yarn/sdks !.yarn/versions +.partykit dist node_modules diff --git a/partykit.json b/partykit.json new file mode 100644 index 000000000..ff146517e --- /dev/null +++ b/partykit.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://www.partykit.io/schema.json", + "name": "ddr-card-draw-party", + "main": "src/party/server.ts", + "compatibilityDate": "2024-06-21" +} diff --git a/src/config-persistence.ts b/src/config-persistence.ts index 3a6d87ebf..9ae2fdaa5 100644 --- a/src/config-persistence.ts +++ b/src/config-persistence.ts @@ -1,8 +1,13 @@ import { ConfigState, useConfigState } from "./config-state"; import { useDrawState } from "./draw-state"; +import { Roomstate } from "./party/types"; import { toaster } from "./toaster"; import { buildDataUri, dateForFilename, shareData } from "./utils/share"; +/** Mark specific fields in T optional, keeping others unchanged */ +type Optional = Partial> & + Omit; + interface PersistedConfigV1 { version: 1; dataSetName: string; @@ -21,7 +26,7 @@ type NonFunctionKeys = keyof { /** * Strips mutations from an object, and converts sets to arrays, maps to arrays of entry pairs */ -type Serialized = { +export type Serialized = { [K in NonFunctionKeys]: T[K] extends ReadonlyMap ? Array<[K, V]> : T[K] extends ReadonlySet @@ -95,14 +100,21 @@ export function loadConfig() { return resolution; } -function buildPersistedConfig(): PersistedConfigV1 { - const { ...configState } = useConfigState.getState(); - const serializedState: PersistedConfigV1["configState"] = { - ...configState, - difficulties: Array.from(configState.difficulties), - flags: Array.from(configState.flags), - folders: Array.from(configState.folders), +export function serializeConfig( + cfg: Optional, +): Serialized { + return { + ...cfg, + difficulties: Array.from(cfg.difficulties), + flags: Array.from(cfg.flags), + folders: Array.from(cfg.folders), }; +} + +function buildPersistedConfig(): PersistedConfigV1 { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { update, ...configState } = useConfigState.getState(); + const serializedState = serializeConfig(configState); const ret: PersistedConfigV1 = { version: 1, dataSetName: useDrawState.getState().dataSetName, @@ -111,7 +123,18 @@ function buildPersistedConfig(): PersistedConfigV1 { return ret; } -async function loadPersistedConfig(saved: PersistedConfigV1) { +export async function loadFromRoomstate(roomstate: Roomstate) { + await loadPersistedConfig({ + version: 1, + dataSetName: roomstate.dataSetName || useDrawState.getState().dataSetName, + configState: roomstate.config, + }); + useDrawState.setState({ drawings: roomstate.drawings }); +} + +async function loadPersistedConfig( + saved: Optional, +) { if (saved.version !== 1) { return false; } @@ -130,11 +153,17 @@ async function loadPersistedConfig(saved: PersistedConfigV1) { await nextConfigChange; } + if (saved.configState) { + applySerializedConfig(saved.configState); + } +} + +export function applySerializedConfig(config: Serialized) { useConfigState.setState({ - ...migrateOldNames(saved.configState), - difficulties: new Set(saved.configState.difficulties), - flags: new Set(saved.configState.flags), - folders: new Set(saved.configState.folders), + ...migrateOldNames(config), + difficulties: new Set(config.difficulties), + flags: new Set(config.flags), + folders: new Set(config.folders), }); } diff --git a/src/config-state.ts b/src/config-state.ts index b24d7ebe7..be0450379 100644 --- a/src/config-state.ts +++ b/src/config-state.ts @@ -1,5 +1,7 @@ import type { StoreApi } from "zustand"; import { createWithEqualityFn } from "zustand/traditional"; +import { sendToParty } from "./party/client"; +import { serializeConfig } from "./config-persistence"; export interface ConfigState { chartCount: number; @@ -29,7 +31,7 @@ export interface ConfigState { } export const useConfigState = createWithEqualityFn( - (set) => ({ + (set, get) => ({ chartCount: 5, playerPicks: 0, upperBound: 0, @@ -61,7 +63,10 @@ export const useConfigState = createWithEqualityFn( sortByLevel: false, defaultPlayersPerDraw: 2, useGranularLevels: false, - update: set, + update: (input) => { + set(input); + sendToParty({ type: "config", config: serializeConfig(get()) }); + }, }), Object.is, ); diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index 060287503..1356b6084 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -8,14 +8,11 @@ import { Divider, FormGroup, HTMLSelect, - Icon, NumericInput, Tab, Tabs, } from "@blueprintjs/core"; import { - ThirdParty, - GlobeNetwork, Settings, People, CaretDown, @@ -35,8 +32,6 @@ import { EligibleChartsListFilter } from "../eligible-charts/filter"; import { useIntl } from "../hooks/useIntl"; import { useIsNarrow } from "../hooks/useMediaQuery"; import { GameData } from "../models/SongData"; -import { RemotePeerControls } from "../tournament-mode/remote-peer-menu"; -import { useRemotePeers } from "../tournament-mode/remote-peers"; import { WeightsControls } from "./controls-weights"; import styles from "./controls.css"; import { PlayerNamesControls } from "./player-names"; @@ -84,8 +79,6 @@ function getDiffsAndRangeForNewStyle( export default function ControlsDrawer() { const { t } = useIntl(); - const isConnected = useRemotePeers((r) => !!r.thisPeer); - const hasPeers = useRemotePeers((r) => !!r.remotePeers.size); return (
@@ -96,24 +89,13 @@ export default function ControlsDrawer() { > {t("controls.tabs.general")} - - ) : ( - - ) - } - intent={isConnected ? "success" : "none"} - /> - } + icon={} />} panel={} > {t("controls.tabs.networking")} - + */} } diff --git a/src/draw-state.tsx b/src/draw-state.tsx index 9503911ae..a28025a00 100644 --- a/src/draw-state.tsx +++ b/src/draw-state.tsx @@ -13,7 +13,7 @@ import { IntlProvider } from "./intl-provider"; import type { StoreApi } from "zustand"; import { createWithEqualityFn } from "zustand/traditional"; import { shallow } from "zustand/shallow"; -import { DataConnection } from "peerjs"; +import { sendToParty } from "./party/client"; interface DrawState { importedData: Map; @@ -27,7 +27,6 @@ interface DrawState { /** returns false if no songs could be drawn */ drawSongs(config: ConfigState): boolean; clearDrawings(): void; - injectRemoteDrawing(d: Drawing, syncWithPeer?: DataConnection): void; } function applyNewData(data: GameData, set: StoreApi["setState"]) { @@ -110,6 +109,10 @@ export const useDrawState = createWithEqualityFn( dataSetName, drawings: [], }); + sendToParty({ + type: "dataSet", + data: dataSetName, + }); writeDataSetToUrl(dataSetName); // Attempt to look up a local data file first @@ -147,28 +150,9 @@ export const useDrawState = createWithEqualityFn( lastDrawFailed: false, }; }); + sendToParty({ type: "drawings", drawings: get().drawings }); return true; }, - injectRemoteDrawing(drawing, syncWithPeer) { - set((prevState) => { - const currentDrawing = prevState.drawings.find( - (d) => d.id === drawing.id, - ); - const newDrawings = prevState.drawings.filter( - (d) => d.id !== drawing.id, - ); - newDrawings.unshift(drawing); - if (currentDrawing) { - drawing.__syncPeer = currentDrawing.__syncPeer; - } - if (syncWithPeer) { - drawing.__syncPeer = syncWithPeer; - } - return { - drawings: newDrawings, - }; - }); - }, }), Object.is, ); diff --git a/src/drawn-set.tsx b/src/drawn-set.tsx index 36b553056..82355047a 100644 --- a/src/drawn-set.tsx +++ b/src/drawn-set.tsx @@ -6,7 +6,6 @@ import { Drawing } from "./models/Drawing"; import { SetLabels } from "./tournament-mode/drawing-labels"; import { DrawingProvider, useDrawing } from "./drawing-context"; import { DrawingActions } from "./tournament-mode/drawing-actions"; -import { SyncWithPeers } from "./tournament-mode/sync-with-peers"; import { useConfigState } from "./config-state"; import { ErrorFallback } from "./utils/error-fallback"; @@ -90,7 +89,6 @@ const DrawnSet = memo(function DrawnSet({ drawing }) {
} > -
; pocketPicks: Array; priorityPlayer?: number; - /** __ prefix avoids serializing this field during sync */ - __syncPeer?: DataConnection; } diff --git a/src/party/README.md b/src/party/README.md new file mode 100644 index 000000000..113b8b895 --- /dev/null +++ b/src/party/README.md @@ -0,0 +1 @@ +This is where the partykit server is implemented. See: https://docs.partykit.io/reference/partyserver-api/ diff --git a/src/party/client.ts b/src/party/client.ts new file mode 100644 index 000000000..7227eb84c --- /dev/null +++ b/src/party/client.ts @@ -0,0 +1,40 @@ +import PartySocket from "partysocket"; +import type { Broadcast, ClientMsg } from "./types"; +import { + applySerializedConfig, + loadFromRoomstate, +} from "../config-persistence"; +import { useDrawState } from "../draw-state"; + +const socket = new PartySocket({ + room: "default", // TODO picked off the path? + host: "localhost:43735", // TODO determine this after deploy, or local? +}); + +socket.addEventListener("message", (evt) => { + try { + const data: Broadcast = JSON.parse(evt.data); + switch (data.type) { + case "roomstate": + loadFromRoomstate(data); + break; + case "config": + applySerializedConfig(data.config); + break; + case "dataSet": + if (typeof data.data === "string") { + useDrawState.getState().loadGameData(data.data); + } + break; + case "drawings": + useDrawState.setState({ drawings: data.drawings }); + break; + } + } catch (e) { + console.warn("failed to handle party socket message", e); + } +}); + +export function sendToParty(message: ClientMsg) { + socket.send(JSON.stringify(message)); +} diff --git a/src/party/server.ts b/src/party/server.ts new file mode 100644 index 000000000..9f5dbb6a2 --- /dev/null +++ b/src/party/server.ts @@ -0,0 +1,77 @@ +import type * as Party from "partykit/server"; +import type { Drawing } from "../models/Drawing"; +import type { ConfigState } from "../config-state"; +import type { Serialized } from "../config-persistence"; +import type { ClientMsg, Roomstate } from "./types"; + +export default class Server implements Party.Server { + private dataSetName?: string; + private drawings: Drawing[] = []; + private config?: Serialized; + + constructor(readonly room: Party.Room) {} + + onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { + // A websocket just connected! + console.log( + `Connected: + id: ${conn.id} + room: ${this.room.id} + url: ${new URL(ctx.request.url).pathname}`, + ); + + // send the initial state to this client + conn.send(this.getRoomState()); + } + + onMessage(message: string, sender: Party.Connection) { + // let's log the message + console.log(`connection ${sender.id} sent message: ${message}`); + // as well as broadcast it to all the other connections in the room... + this.room.broadcast( + message, + // ...except for the connection it came from + [sender.id], + ); + + const parsed = JSON.parse(message) as ClientMsg; + switch (parsed.type) { + case "config": + this.config = parsed.config; + this.persistConfig(); + break; + case "drawings": + this.drawings = parsed.drawings; + this.room.storage.put("allDrawings", this.drawings); + break; + case "dataSet": + if (typeof parsed.data === "string") { + this.dataSetName = parsed.data; + } else { + console.error(`party server does not yet support custom data`); + } + break; + } + } + + private getRoomState() { + return JSON.stringify({ + type: "roomstate", + drawings: this.drawings, + config: this.config, + dataSetName: this.dataSetName, + } satisfies Roomstate); + } + + private persistConfig() { + this.room.storage.put("config", this.config); + } + + private async restoreFromStorage() { + this.config = await this.room.storage.get("config"); + this.drawings = + (await this.room.storage.get(`allDrawings`)) || []; + } +} + +Server satisfies Party.Worker; diff --git a/src/party/types.ts b/src/party/types.ts new file mode 100644 index 000000000..339ef555a --- /dev/null +++ b/src/party/types.ts @@ -0,0 +1,31 @@ +import type { Serialized } from "../config-persistence"; +import type { ConfigState } from "../config-state"; +import type { Drawing } from "../models/Drawing"; +import type { GameData } from "../models/SongData"; + +export interface Roomstate { + type: "roomstate"; + dataSetName?: string; + drawings: Drawing[]; + config?: Serialized; +} + +/** All messages possibly sent by the server to clients */ +export type Broadcast = Roomstate | ClientMsg; + +export interface DrawingUpdate { + type: "drawings"; + drawings: Drawing[]; +} + +export interface ConfigChange { + type: "config"; + config: Serialized; +} + +export interface DataSetChange { + type: "dataSet"; + data: string | GameData; +} + +export type ClientMsg = DrawingUpdate | ConfigChange | DataSetChange; diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index 7086238d7..f881b3a31 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -1,28 +1,15 @@ +import { Button, Tooltip } from "@blueprintjs/core"; import { - Button, - Icon, - Menu, - MenuItem, - Popover, - Tooltip, -} from "@blueprintjs/core"; -import { - SendMessage, - Changes, - Share, Camera, Refresh, NewPerson, BlockedPerson, Error, } from "@blueprintjs/icons"; -import { useDrawing, useDrawingStore } from "../drawing-context"; +import { useDrawing } from "../drawing-context"; import styles from "./drawing-actions.css"; -import { CurrentPeersMenu } from "./remote-peer-menu"; -import { displayFromPeerId, useRemotePeers } from "./remote-peers"; import { domToPng } from "modern-screenshot"; import { shareImage } from "../utils/share"; -import { firstOf } from "../utils"; import { useConfigState } from "../config-state"; import { useErrorBoundary } from "react-error-boundary"; @@ -33,67 +20,11 @@ export function DrawingActions() { const updateDrawing = useDrawing((s) => s.updateDrawing); const redrawAllCharts = useDrawing((s) => s.redrawAllCharts); const hasPlayers = useDrawing((s) => !!s.players.length); - const syncPeer = useDrawing((s) => s.__syncPeer); - const isConnected = useRemotePeers((s) => !!s.thisPeer); - const remotePeers = useRemotePeers((s) => s.remotePeers); - const sendDrawing = useRemotePeers((s) => s.sendDrawing); - const syncDrawing = useRemotePeers((s) => s.beginSyncWithPeer); - const drawingStore = useDrawingStore(); const showLabels = useConfigState((s) => s.showPlayerAndRoundLabels); const { showBoundary } = useErrorBoundary(); - let remoteActions: JSX.Element | undefined = undefined; - - const onlyRemote = firstOf(remotePeers.values()); - if (remotePeers.size === 1 && onlyRemote) { - const peerId = displayFromPeerId(onlyRemote.peer); - remoteActions = ( - - } - text={`Send to ${peerId}`} - onClick={() => sendDrawing(getDrawing())} - /> - } - text={`Start sync with ${peerId}`} - onClick={() => syncDrawing(drawingStore)} - /> - - ); - } else if (remotePeers.size > 1) { - remoteActions = ( - - } text="Send to..."> - sendDrawing(getDrawing(), peerId)} - /> - - } text="Start sync with..."> - syncDrawing(drawingStore, peerId)} - /> - - - ); - } - - const button = ( - @@ -234,7 +234,7 @@ function FolderSettings() { key={`${dataSetName}:${idx}`} label={folder} value={folder} - checked={selectedFolders.has(folder)} + checked={selectedFolders.includes(folder)} onChange={() => updateState((s) => { const newFolders = new Set(s.folders); @@ -243,7 +243,7 @@ function FolderSettings() { } else { newFolders.add(folder); } - return { folders: newFolders }; + return { folders: Array.from(newFolders) }; }) } /> @@ -345,7 +345,9 @@ function GeneralSettings() { - + @@ -463,7 +465,7 @@ function GeneralSettings() { next.style, ); if (diffs.length === 1) { - next.difficulties = new Set(diffs.map((d) => d.key)); + next.difficulties = diffs.map((d) => d.key); } if (lvlRange.low > next.upperBound) { next.upperBound = lvlRange.low; @@ -489,7 +491,7 @@ function GeneralSettings() { key={`${dif.key}`} name="difficulties" value={dif.key} - checked={selectedDifficulties.has(dif.key)} + checked={selectedDifficulties.includes(dif.key)} onChange={(e) => { const { checked, value } = e.currentTarget; updateState((s) => { @@ -499,7 +501,7 @@ function GeneralSettings() { } else { difficulties.delete(value); } - return { difficulties }; + return { difficulties: Array.from(difficulties) }; }); }} label={t("meta." + dif.key)} diff --git a/src/eligible-charts/index.tsx b/src/eligible-charts/index.tsx index 9faef5967..9bdd1b17e 100644 --- a/src/eligible-charts/index.tsx +++ b/src/eligible-charts/index.tsx @@ -67,7 +67,7 @@ export default function EligibleChartsList() { {charts.length} eligible charts from {songs.size} songs (of{" "} {gameData.songs.length} total) - {configState.flags.size > 0 && !isNarrow && ( + {configState.flags.length > 0 && !isNarrow && ( diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index 105dccb4b..d723c364a 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -6,6 +6,7 @@ import { gameDataSlice } from "./game-data.slice"; import { store, useAppDispatch, useAppState } from "./store"; import { EqualityFn } from "react-redux"; import { addPlayerNameToDrawing } from "./central"; +import { useCallback } from "react"; function createPartialFromDefaults(gameData: GameData) { const { lowerLvlBound, upperLvlBound, flags, difficulties, folders, style } = @@ -13,9 +14,9 @@ function createPartialFromDefaults(gameData: GameData) { const ret: Partial = { lowerBound: lowerLvlBound, upperBound: upperLvlBound, - flags: new Set(flags), - difficulties: new Set(difficulties), - folders: new Set(folders), + flags, + difficulties, + folders, style, cutoffDate: "", }; @@ -70,15 +71,18 @@ export function useConfigState( export function useUpdateConfig() { const dispatch = useAppDispatch(); - return ( - patch: - | Partial - | ((state: ConfigState) => Partial), - ) => { - if (typeof patch === "function") { - const state = configSlice.selectSlice(store.getState()); - patch = patch(state); - } - dispatch(actions.update(patch)); - }; + return useCallback( + ( + patch: + | Partial + | ((state: ConfigState) => Partial), + ) => { + if (typeof patch === "function") { + const state = configSlice.selectSlice(store.getState()); + patch = patch(state); + } + dispatch(actions.update(patch)); + }, + [dispatch], + ); } From 18305be2cdb863f041b83e0ace1f2855ba350d90 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 3 Jul 2024 17:26:09 -0700 Subject: [PATCH 008/168] speculative wireup for new partykit networking --- src/config-persistence.ts | 158 ++++++------------------------- src/controls/index.tsx | 8 +- src/party/client.ts | 81 +++++++++------- src/party/server.ts | 61 +++++------- src/party/types.ts | 32 ++----- src/state/drawings.slice.ts | 20 ++-- src/state/listener-middleware.ts | 9 ++ src/state/root-reducer.ts | 22 +++++ src/state/store.ts | 11 ++- 9 files changed, 155 insertions(+), 247 deletions(-) create mode 100644 src/state/listener-middleware.ts create mode 100644 src/state/root-reducer.ts diff --git a/src/config-persistence.ts b/src/config-persistence.ts index 0603b43b0..5bd278dfb 100644 --- a/src/config-persistence.ts +++ b/src/config-persistence.ts @@ -1,24 +1,16 @@ -import { getDefaultStore } from "jotai"; -import { - ConfigState, - showEligibleCharts, - showPlayerAndRoundLabels, -} from "./config-state"; -import { useDrawState } from "./draw-state"; -import { Roomstate } from "./party/types"; -import { store } from "./state/store"; +// import { ConfigState } from "./config-state"; import { toaster } from "./toaster"; -import { buildDataUri, dateForFilename, shareData } from "./utils/share"; +// import { buildDataUri, dateForFilename, shareData } from "./utils/share"; /** Mark specific fields in T optional, keeping others unchanged */ -type Optional = Partial> & - Omit; +// type Optional = Partial> & +// Omit; -interface PersistedConfigV1 { - version: 1; - dataSetName: string; - configState: Serialized & OldSettings; -} +// interface PersistedConfigV1 { +// version: 1; +// dataSetName: string; +// configState: Serialized; +// } /** * Returns a union of all property names in T which do not contain a function value. @@ -40,22 +32,22 @@ export type Serialized = { : T[K]; }; -export function saveConfig() { - const persistedObj = buildPersistedConfig(store.getState().config); - const dataUri = buildDataUri( - JSON.stringify(persistedObj, undefined, 2), - "application/json", - "url", - ); - - return shareData(dataUri, { - filename: `ddr-tools-config-${persistedObj.dataSetName}-${dateForFilename()}.json`, - methods: [ - { type: "nativeShare", allowDesktop: true }, - { type: "download" }, - ], - }); -} +// export function saveConfig() { +// const persistedObj = buildPersistedConfig(store.getState().config); +// const dataUri = buildDataUri( +// JSON.stringify(persistedObj, undefined, 2), +// "application/json", +// "url", +// ); + +// return shareData(dataUri, { +// filename: `ddr-tools-config-${persistedObj.dataSetName}-${dateForFilename()}.json`, +// methods: [ +// { type: "nativeShare", allowDesktop: true }, +// { type: "download" }, +// ], +// }); +// } export function loadConfig() { const fileInput = document.createElement("input"); @@ -80,8 +72,8 @@ export function loadConfig() { reject(); throw new Error("file type is " + f.type); } - const contents: PersistedConfigV1 = JSON.parse(await f.text()); - await loadPersistedConfig(contents); + // const contents: PersistedConfigV1 = JSON.parse(await f.text()); + // await loadPersistedConfig(contents); resolve(); toaster.show({ message: "Successfully loaded draw settings", @@ -105,99 +97,3 @@ export function loadConfig() { fileInput.click(); return resolution; } - -export function serializeConfig(cfg: ConfigState): Serialized { - return { - ...cfg, - difficulties: Array.from(cfg.difficulties), - flags: Array.from(cfg.flags), - folders: Array.from(cfg.folders), - }; -} - -function buildPersistedConfig(cfg: ConfigState): PersistedConfigV1 { - const serializedState = serializeConfig(cfg); - const ret: PersistedConfigV1 = { - version: 1, - dataSetName: useDrawState.getState().dataSetName, - configState: serializedState, - }; - return ret; -} - -export async function loadFromRoomstate(roomstate: Roomstate) { - await loadPersistedConfig({ - version: 1, - dataSetName: roomstate.dataSetName || useDrawState.getState().dataSetName, - configState: roomstate.config, - }); - useDrawState.setState({ drawings: roomstate.drawings }); -} - -async function loadPersistedConfig( - saved: Optional, -) { - if (saved.version !== 1) { - return false; - } - const drawState = useDrawState.getState(); - if (drawState.dataSetName !== saved.dataSetName) { - const nextConfigChange = new Promise((resolve) => { - const unsub = useConfigState.subscribe(() => { - unsub(); - resolve(); - }); - }); - await drawState.loadGameData(saved.dataSetName); - // the ApplyDefaultConfig component will kick in - // to overwrite config in response to this change - // so we have to wait for that to happen before continuing - await nextConfigChange; - } - - if (saved.configState) { - applySerializedConfig(saved.configState); - } -} - -export function applySerializedConfig(config: Serialized) { - useConfigState.setState({ - ...migrateOldNames(config), - difficulties: new Set(config.difficulties), - flags: new Set(config.flags), - folders: new Set(config.folders), - }); -} - -interface OldSettings { - /** renamed to `showEligibleCharts` */ - showPool?: boolean; - /** renamed to `showPlayerAndRoundLabels` */ - showLabels?: boolean; -} - -function migrateOldNames( - config: PersistedConfigV1["configState"], -): Serialized { - const { showPool, showLabels, ...modernConfig } = config; - - const jotaiStore = getDefaultStore(); - if (showPool) { - jotaiStore.set(showEligibleCharts, showPool); - } - - if (showLabels) { - jotaiStore.set(showPlayerAndRoundLabels, showLabels); - } - - const maybeOldWeights = modernConfig.weights as unknown as - | Array<[number, number]> - | Array; - if (Array.isArray(maybeOldWeights[0])) { - modernConfig.weights = maybeOldWeights.map((pair) => - Array.isArray(pair) ? pair[1] : pair, - ); - } - - return modernConfig; -} diff --git a/src/controls/index.tsx b/src/controls/index.tsx index a43d0f1ff..b62ad0ff9 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -9,11 +9,11 @@ import { Spinner, Tooltip, } from "@blueprintjs/core"; -import { NewLayers, Cog, FloppyDisk, Import } from "@blueprintjs/icons"; +import { NewLayers, Cog } from "@blueprintjs/icons"; import { useState, lazy, Suspense } from "react"; import { FormattedMessage } from "react-intl"; import { useIsNarrow } from "../hooks/useMediaQuery"; -import { loadConfig, saveConfig } from "../config-persistence"; +// import { loadConfig, saveConfig } from "../config-persistence"; import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "../utils/error-fallback"; import { ShowChartsToggle } from "./show-charts-toggle"; @@ -59,14 +59,14 @@ export function HeaderControls() { title={ <> - + {/* - + */} } > diff --git a/src/party/client.ts b/src/party/client.ts index 7227eb84c..704dadb15 100644 --- a/src/party/client.ts +++ b/src/party/client.ts @@ -1,40 +1,49 @@ -import PartySocket from "partysocket"; -import type { Broadcast, ClientMsg } from "./types"; -import { - applySerializedConfig, - loadFromRoomstate, -} from "../config-persistence"; -import { useDrawState } from "../draw-state"; +import usePartySocket from "partysocket/react"; +import type { Broadcast, ReduxAction } from "./types"; +import { useAppDispatch } from "../state/store"; +import { receivePartyState } from "../state/central"; +import { startAppListening } from "../state/listener-middleware"; +import { useEffect } from "react"; -const socket = new PartySocket({ - room: "default", // TODO picked off the path? - host: "localhost:43735", // TODO determine this after deploy, or local? -}); - -socket.addEventListener("message", (evt) => { - try { - const data: Broadcast = JSON.parse(evt.data); - switch (data.type) { - case "roomstate": - loadFromRoomstate(data); - break; - case "config": - applySerializedConfig(data.config); - break; - case "dataSet": - if (typeof data.data === "string") { - useDrawState.getState().loadGameData(data.data); +export function PartySocketManager(props: { roomName?: string }) { + const dispatch = useAppDispatch(); + const socket = usePartySocket({ + room: props.roomName, + host: "localhost:43735", // TODO determine this based on build type, other stuff + onMessage(evt) { + try { + const data: Broadcast = JSON.parse(evt.data); + switch (data.type) { + case "roomstate": + dispatch(receivePartyState(data)); + break; + case "action": + const foreignAction = { + ...data.action, + meta: { source: "partykit" }, + }; + dispatch(foreignAction); + break; } - break; - case "drawings": - useDrawState.setState({ drawings: data.drawings }); - break; - } - } catch (e) { - console.warn("failed to handle party socket message", e); - } -}); + } catch (e) { + console.warn("failed to handle party socket message", e); + } + }, + }); -export function sendToParty(message: ClientMsg) { - socket.send(JSON.stringify(message)); + useEffect(() => { + return startAppListening({ + predicate(action) { + // @ts-expect-error i don't know how to type action meta properties yet + return action.meta.source !== "partykit"; + }, + effect(action) { + const message: ReduxAction = { + type: "action", + action, + }; + socket.send(JSON.stringify(message)); + }, + }); + }, [socket]); } diff --git a/src/party/server.ts b/src/party/server.ts index 9f5dbb6a2..33c85988c 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -1,16 +1,19 @@ import type * as Party from "partykit/server"; -import type { Drawing } from "../models/Drawing"; -import type { ConfigState } from "../config-state"; -import type { Serialized } from "../config-persistence"; -import type { ClientMsg, Roomstate } from "./types"; +import type { ReduxAction, Roomstate } from "./types"; +import { Action, configureStore } from "@reduxjs/toolkit"; +import { reducer } from "../state/root-reducer"; +import { AppState } from "../state/store"; +import { receivePartyState } from "../state/central"; export default class Server implements Party.Server { - private dataSetName?: string; - private drawings: Drawing[] = []; - private config?: Serialized; + private store = configureStore({ reducer }); constructor(readonly room: Party.Room) {} + onStart(): void | Promise { + return this.restoreFromStorage(); + } + onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { // A websocket just connected! console.log( @@ -34,43 +37,31 @@ export default class Server implements Party.Server { [sender.id], ); - const parsed = JSON.parse(message) as ClientMsg; - switch (parsed.type) { - case "config": - this.config = parsed.config; - this.persistConfig(); - break; - case "drawings": - this.drawings = parsed.drawings; - this.room.storage.put("allDrawings", this.drawings); - break; - case "dataSet": - if (typeof parsed.data === "string") { - this.dataSetName = parsed.data; - } else { - console.error(`party server does not yet support custom data`); - } - break; - } + const parsed = JSON.parse(message) as ReduxAction; + this.dispatchAndPersist(parsed); } private getRoomState() { - return JSON.stringify({ + return JSON.stringify({ type: "roomstate", - drawings: this.drawings, - config: this.config, - dataSetName: this.dataSetName, - } satisfies Roomstate); + state: this.store.getState(), + }); } - private persistConfig() { - this.room.storage.put("config", this.config); + private async dispatchAndPersist(action: Action) { + this.store.dispatch(action); + await this.room.storage.put("currentState", this.store.getState()); } private async restoreFromStorage() { - this.config = await this.room.storage.get("config"); - this.drawings = - (await this.room.storage.get(`allDrawings`)) || []; + const preloadedState = + await this.room.storage.get("currentState"); + if (preloadedState) { + this.store.dispatch( + receivePartyState({ type: "roomstate", state: preloadedState }), + ); + } + // this.store = configureStore({ reducer, preloadedState }); } } diff --git a/src/party/types.ts b/src/party/types.ts index 339ef555a..7aec1910e 100644 --- a/src/party/types.ts +++ b/src/party/types.ts @@ -1,31 +1,15 @@ -import type { Serialized } from "../config-persistence"; -import type { ConfigState } from "../config-state"; -import type { Drawing } from "../models/Drawing"; -import type { GameData } from "../models/SongData"; +import { Action } from "@reduxjs/toolkit"; +import { AppState } from "../state/store"; export interface Roomstate { type: "roomstate"; - dataSetName?: string; - drawings: Drawing[]; - config?: Serialized; + state: AppState; } -/** All messages possibly sent by the server to clients */ -export type Broadcast = Roomstate | ClientMsg; - -export interface DrawingUpdate { - type: "drawings"; - drawings: Drawing[]; -} - -export interface ConfigChange { - type: "config"; - config: Serialized; +export interface ReduxAction { + type: "action"; + action: Action; } -export interface DataSetChange { - type: "dataSet"; - data: string | GameData; -} - -export type ClientMsg = DrawingUpdate | ConfigChange | DataSetChange; +/** All messages possibly sent by the server to clients */ +export type Broadcast = Roomstate | ReduxAction; diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 7a7305267..811e928a7 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -9,7 +9,7 @@ import { EligibleChart, PlayerActionOnChart, } from "../models/Drawing"; -import { addPlayerNameToDrawing, receivePartyState } from "./central"; +import { addPlayerNameToDrawing } from "./central"; import { AppState } from "./store"; import { draw } from "../card-draw"; @@ -154,17 +154,13 @@ export const drawingsSlice = createSlice({ }, }, extraReducers(builder) { - builder - .addCase(receivePartyState, (state, action) => { - return drawingsAdapter.addMany(state, action.payload.drawings); - }) - .addCase(addPlayerNameToDrawing, (state, action) => { - const drawing = state.entities[action.payload.drawingId]; - if (!drawing) { - return; - } - drawing.players[action.payload.asPlayerNo - 1] = action.payload.name; - }); + builder.addCase(addPlayerNameToDrawing, (state, action) => { + const drawing = state.entities[action.payload.drawingId]; + if (!drawing) { + return; + } + drawing.players[action.payload.asPlayerNo - 1] = action.payload.name; + }); }, selectors: { haveDrawings(state) { diff --git a/src/state/listener-middleware.ts b/src/state/listener-middleware.ts new file mode 100644 index 000000000..9184fa02c --- /dev/null +++ b/src/state/listener-middleware.ts @@ -0,0 +1,9 @@ +import { createListenerMiddleware } from "@reduxjs/toolkit"; +import type { AppState, AppDispatch } from "./store"; + +export const listenerMiddleware = createListenerMiddleware(); + +export const startAppListening = listenerMiddleware.startListening.withTypes< + AppState, + AppDispatch +>(); diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts new file mode 100644 index 000000000..9a8954b81 --- /dev/null +++ b/src/state/root-reducer.ts @@ -0,0 +1,22 @@ +import { combineSlices } from "@reduxjs/toolkit"; +import { configSlice } from "./config.slice"; +import { drawingsSlice } from "./drawings.slice"; +import { gameDataSlice } from "./game-data.slice"; +import { receivePartyState } from "./central"; + +const combinedReducer = combineSlices( + drawingsSlice, + configSlice, + gameDataSlice, +); + +export const reducer: typeof combinedReducer = (state, action) => { + if (receivePartyState.match(action)) { + return action.payload.state; + } + return combinedReducer(state, action); +}; + +reducer.inject = combinedReducer.inject; +reducer.withLazyLoadedSlices = combinedReducer.withLazyLoadedSlices; +reducer.selector = combinedReducer.selector; diff --git a/src/state/store.ts b/src/state/store.ts index ea03cde5b..058c3f6b4 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -1,11 +1,12 @@ -import { configureStore, combineSlices } from "@reduxjs/toolkit"; +import { configureStore } from "@reduxjs/toolkit"; import { useDispatch, useSelector, useStore } from "react-redux"; -import { drawingsSlice } from "./drawings.slice"; -import { configSlice } from "./config.slice"; -import { gameDataSlice } from "./game-data.slice"; +import { reducer } from "./root-reducer"; +import { listenerMiddleware } from "./listener-middleware"; export const store = configureStore({ - reducer: combineSlices(drawingsSlice, configSlice, gameDataSlice), + reducer, + middleware: (getDefaults) => + getDefaults().concat(listenerMiddleware.middleware), }); export type AppState = ReturnType; From 82a22c9b116519422da88ef8612651a6f37e9bef Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 3 Jul 2024 18:56:55 -0700 Subject: [PATCH 009/168] reorganize further so server can build --- src/app.tsx | 2 ++ src/controls/controls-drawer.tsx | 2 +- src/controls/controls-weights.tsx | 2 +- src/controls/player-names.tsx | 2 +- src/eligible-charts/filter.tsx | 2 +- src/eligible-charts/histogram.tsx | 2 +- src/eligible-charts/index.tsx | 2 +- src/party/client.ts | 21 +++++++++++++--- src/party/server.ts | 26 +++++++++----------- src/song-card/chart-level.tsx | 2 +- src/song-card/song-card.tsx | 2 +- src/song-search/index.tsx | 2 +- src/state/central.ts | 4 +-- src/state/config.slice.ts | 34 -------------------------- src/state/hooks.ts | 34 ++++++++++++++++++++++++++ src/state/root-reducer.ts | 2 +- src/tournament-mode/drawing-labels.tsx | 2 +- src/tournament-mode/round-select.tsx | 2 +- src/utils/index.ts | 4 ++- 19 files changed, 83 insertions(+), 66 deletions(-) create mode 100644 src/state/hooks.ts diff --git a/src/app.tsx b/src/app.tsx index 57aa98933..390b2f8e7 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -16,10 +16,12 @@ import { ThemeSyncWidget } from "./theme-toggle"; import { DropHandler } from "./drop-handler"; import { Provider } from "react-redux"; import { store } from "./state/store"; +import { PartySocketManager } from "./party/client"; export function App() { return ( + diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index 542b62f57..3fe4fd30a 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -36,7 +36,7 @@ import { getAvailableLevels } from "../game-data-utils"; import { ShowChartsToggle } from "./show-charts-toggle"; import { Fraction } from "../utils/fraction"; import { detectedLanguage } from "../utils"; -import { useConfigState, useUpdateConfig } from "../state/config.slice"; +import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useAtomValue } from "jotai"; import { showEligibleCharts } from "../config-state"; import { useAppState } from "../state/store"; diff --git a/src/controls/controls-weights.tsx b/src/controls/controls-weights.tsx index 2dfc767f5..8c30260f1 100644 --- a/src/controls/controls-weights.tsx +++ b/src/controls/controls-weights.tsx @@ -2,7 +2,7 @@ import { shallow } from "zustand/shallow"; import styles from "./controls-weights.css"; import { zeroPad } from "../utils"; import { useMemo } from "react"; -import { useConfigState, useUpdateConfig } from "../state/config.slice"; +import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useIntl } from "../hooks/useIntl"; import { NumericInput, Checkbox, Classes } from "@blueprintjs/core"; import { getAvailableLevels } from "../game-data-utils"; diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 29780ca94..359a16735 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -6,7 +6,7 @@ import { TagInput, } from "@blueprintjs/core"; import { ReactNode } from "react"; -import { useConfigState, useUpdateConfig } from "../state/config.slice"; +import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useIntl } from "../hooks/useIntl"; import { DiagramTree, Person } from "@blueprintjs/icons"; import { useAtom } from "jotai"; diff --git a/src/eligible-charts/filter.tsx b/src/eligible-charts/filter.tsx index 2b469aff7..e34e666cd 100644 --- a/src/eligible-charts/filter.tsx +++ b/src/eligible-charts/filter.tsx @@ -1,6 +1,6 @@ import { HTMLSelect } from "@blueprintjs/core"; import { atom, useAtom } from "jotai"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { useIntl } from "../hooks/useIntl"; export const currentTabAtom = atom("all"); diff --git a/src/eligible-charts/histogram.tsx b/src/eligible-charts/histogram.tsx index 50d87f777..2f1025c16 100644 --- a/src/eligible-charts/histogram.tsx +++ b/src/eligible-charts/histogram.tsx @@ -18,7 +18,7 @@ import { } from "../game-data-utils"; import { Theme, useTheme } from "../theme-toggle"; import { useIsNarrow } from "../hooks/useMediaQuery"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { useAppState } from "../state/store"; interface Props { diff --git a/src/eligible-charts/index.tsx b/src/eligible-charts/index.tsx index 9bdd1b17e..64d9f6801 100644 --- a/src/eligible-charts/index.tsx +++ b/src/eligible-charts/index.tsx @@ -1,5 +1,5 @@ import { eligibleCharts } from "../card-draw"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { SongCard } from "../song-card"; import styles from "../drawing-list.css"; import { EligibleChart } from "../models/Drawing"; diff --git a/src/party/client.ts b/src/party/client.ts index 704dadb15..bfcc7f1fa 100644 --- a/src/party/client.ts +++ b/src/party/client.ts @@ -4,18 +4,19 @@ import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; import { useEffect } from "react"; +import { loadGameDataByName } from "../state/thunks"; export function PartySocketManager(props: { roomName?: string }) { const dispatch = useAppDispatch(); const socket = usePartySocket({ room: props.roomName, - host: "localhost:43735", // TODO determine this based on build type, other stuff + host: "localhost:1999", // TODO determine this based on build type, other stuff onMessage(evt) { try { const data: Broadcast = JSON.parse(evt.data); switch (data.type) { case "roomstate": - dispatch(receivePartyState(data)); + dispatch(receivePartyState(data.state)); break; case "action": const foreignAction = { @@ -35,7 +36,19 @@ export function PartySocketManager(props: { roomName?: string }) { return startAppListening({ predicate(action) { // @ts-expect-error i don't know how to type action meta properties yet - return action.meta.source !== "partykit"; + if (action.meta?.source === "partykit") { + return false; + } + + if (receivePartyState.match(action)) { + return false; + } + + // don't try sending the loaded game data through + if (action.type.startsWith(loadGameDataByName.typePrefix)) { + return false; + } + return true; }, effect(action) { const message: ReduxAction = { @@ -46,4 +59,6 @@ export function PartySocketManager(props: { roomName?: string }) { }, }); }, [socket]); + + return null; } diff --git a/src/party/server.ts b/src/party/server.ts index 33c85988c..f57281859 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -1,6 +1,6 @@ import type * as Party from "partykit/server"; import type { ReduxAction, Roomstate } from "./types"; -import { Action, configureStore } from "@reduxjs/toolkit"; +import { configureStore } from "@reduxjs/toolkit"; import { reducer } from "../state/root-reducer"; import { AppState } from "../state/store"; import { receivePartyState } from "../state/central"; @@ -8,10 +8,16 @@ import { receivePartyState } from "../state/central"; export default class Server implements Party.Server { private store = configureStore({ reducer }); - constructor(readonly room: Party.Room) {} + constructor(readonly room: Party.Room) { + console.log("constructor start"); + } - onStart(): void | Promise { - return this.restoreFromStorage(); + async onStart() { + console.log("onStart", typeof reducer); + await this.restoreFromStorage(); + this.store.subscribe(() => { + this.room.storage.put("currentState", this.store.getState()); + }); } onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { @@ -38,7 +44,7 @@ export default class Server implements Party.Server { ); const parsed = JSON.parse(message) as ReduxAction; - this.dispatchAndPersist(parsed); + this.store.dispatch(parsed.action); } private getRoomState() { @@ -48,20 +54,12 @@ export default class Server implements Party.Server { }); } - private async dispatchAndPersist(action: Action) { - this.store.dispatch(action); - await this.room.storage.put("currentState", this.store.getState()); - } - private async restoreFromStorage() { const preloadedState = await this.room.storage.get("currentState"); if (preloadedState) { - this.store.dispatch( - receivePartyState({ type: "roomstate", state: preloadedState }), - ); + this.store.dispatch(receivePartyState(preloadedState)); } - // this.store = configureStore({ reducer, preloadedState }); } } diff --git a/src/song-card/chart-level.tsx b/src/song-card/chart-level.tsx index 6336b5108..ea52f3ada 100644 --- a/src/song-card/chart-level.tsx +++ b/src/song-card/chart-level.tsx @@ -1,4 +1,4 @@ -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { formatLevel } from "../game-data-utils"; import { CHART_PLACEHOLDER, diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index 0ba725718..086b0db64 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -1,7 +1,7 @@ import { Popover } from "@blueprintjs/core"; import classNames from "classnames"; import { useCallback, useMemo, useState } from "react"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { useDrawing } from "../drawing-context"; import { CHART_PLACEHOLDER, diff --git a/src/song-search/index.tsx b/src/song-search/index.tsx index 525914508..ec3e58a96 100644 --- a/src/song-search/index.tsx +++ b/src/song-search/index.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { chartIsValid, getDrawnChart, songIsValid } from "../card-draw"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { EligibleChart } from "../models/Drawing"; import { Song } from "../models/SongData"; import { SearchResult, SearchResultData } from "./search-result"; diff --git a/src/state/central.ts b/src/state/central.ts index 69de34f0c..6096443a1 100644 --- a/src/state/central.ts +++ b/src/state/central.ts @@ -1,10 +1,10 @@ import { createAction } from "@reduxjs/toolkit"; import { withPayload } from "./util"; -import { Roomstate } from "../party/types"; +import type { AppState } from "./store"; export const receivePartyState = createAction( "party/supplyState", - withPayload(), + withPayload(), ); export const addPlayerNameToDrawing = createAction( diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index d723c364a..1ce348945 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -3,10 +3,7 @@ import { ConfigState, initialState } from "../config-state"; import { loadGameDataByName } from "./thunks"; import { GameData } from "../models/SongData"; import { gameDataSlice } from "./game-data.slice"; -import { store, useAppDispatch, useAppState } from "./store"; -import { EqualityFn } from "react-redux"; import { addPlayerNameToDrawing } from "./central"; -import { useCallback } from "react"; function createPartialFromDefaults(gameData: GameData) { const { lowerLvlBound, upperLvlBound, flags, difficulties, folders, style } = @@ -55,34 +52,3 @@ export const configSlice = createSlice({ }); }, }); - -export const { actions } = configSlice; - -export function useConfigState( - selector?: (state: ConfigState) => T, - equalityFn?: EqualityFn, -) { - return useAppState((state) => { - const sliceState = configSlice.selectSlice(state); - if (!selector) return sliceState as T; - return selector(sliceState); - }, equalityFn); -} - -export function useUpdateConfig() { - const dispatch = useAppDispatch(); - return useCallback( - ( - patch: - | Partial - | ((state: ConfigState) => Partial), - ) => { - if (typeof patch === "function") { - const state = configSlice.selectSlice(store.getState()); - patch = patch(state); - } - dispatch(actions.update(patch)); - }, - [dispatch], - ); -} diff --git a/src/state/hooks.ts b/src/state/hooks.ts new file mode 100644 index 000000000..e15d142bf --- /dev/null +++ b/src/state/hooks.ts @@ -0,0 +1,34 @@ +import { store, useAppDispatch, useAppState } from "./store"; +import { EqualityFn } from "react-redux"; +import { useCallback } from "react"; +import { ConfigState } from "../config-state"; +import { configSlice } from "./config.slice"; + +export function useConfigState( + selector?: (state: ConfigState) => T, + equalityFn?: EqualityFn, +) { + return useAppState((state) => { + const sliceState = configSlice.selectSlice(state); + if (!selector) return sliceState as T; + return selector(sliceState); + }, equalityFn); +} + +export function useUpdateConfig() { + const dispatch = useAppDispatch(); + return useCallback( + ( + patch: + | Partial + | ((state: ConfigState) => Partial), + ) => { + if (typeof patch === "function") { + const state = configSlice.selectSlice(store.getState()); + patch = patch(state); + } + dispatch(configSlice.actions.update(patch)); + }, + [dispatch], + ); +} diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index 9a8954b81..b0af54bc6 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -12,7 +12,7 @@ const combinedReducer = combineSlices( export const reducer: typeof combinedReducer = (state, action) => { if (receivePartyState.match(action)) { - return action.payload.state; + return action.payload; } return combinedReducer(state, action); }; diff --git a/src/tournament-mode/drawing-labels.tsx b/src/tournament-mode/drawing-labels.tsx index 94cd7e757..b9b2bbb82 100644 --- a/src/tournament-mode/drawing-labels.tsx +++ b/src/tournament-mode/drawing-labels.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { useDrawing } from "../drawing-context"; import styles from "./drawing-labels.css"; import { AutoCompleteSelect, RoundSelect } from "./round-select"; diff --git a/src/tournament-mode/round-select.tsx b/src/tournament-mode/round-select.tsx index e5d96c3df..3b323b85c 100644 --- a/src/tournament-mode/round-select.tsx +++ b/src/tournament-mode/round-select.tsx @@ -5,7 +5,7 @@ import { filterRoundLabel, renderRoundLabel } from "./round-label"; import { useDrawing, useUpdateDrawing } from "../drawing-context"; import { useIntl } from "../hooks/useIntl"; import { useMemo } from "react"; -import { useConfigState } from "../state/config.slice"; +import { useConfigState } from "../state/hooks"; import { Add } from "@blueprintjs/icons"; function identity(i: T) { diff --git a/src/utils/index.ts b/src/utils/index.ts index af0a60968..1779fb57e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -72,7 +72,9 @@ interface GameDataParent { /** ordered list of all available game data files */ export const availableGameData = ( - process.env.DATA_FILES as Array> + (process.env.DATA_FILES as Array< + Omit + >) || [] ).sort((a, b) => { const parentDiff = a.parent.localeCompare(b.parent); if (parentDiff) { From 8c763ebfdcf2f11e1f04f1d6ebebe07a802669eb Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 12:11:31 -0700 Subject: [PATCH 010/168] move game data loading out of redux --- src/controls/controls-drawer.tsx | 12 +++--- src/controls/controls-weights.tsx | 5 ++- src/controls/degrs-tester.tsx | 8 ++-- src/controls/index.tsx | 8 ++-- src/draw-state.tsx | 48 ++++++++------------- src/drop-handler.tsx | 17 ++++++-- src/eligible-charts/histogram.tsx | 5 ++- src/eligible-charts/index.tsx | 6 +-- src/hooks/useDataSets.ts | 37 ++++++++-------- src/hooks/useFuzzySearch.ts | 5 ++- src/last-update.tsx | 4 +- src/party/client.ts | 5 --- src/song-search/index.tsx | 6 +-- src/state/config.slice.ts | 70 ++++++++++++++----------------- src/state/drawings.slice.ts | 16 +++++-- src/state/game-data.atoms.ts | 68 ++++++++++++++++++++++++++++++ src/state/game-data.slice.ts | 31 ++------------ src/state/thunks.ts | 25 ----------- src/version-select.tsx | 27 ++++++++---- 19 files changed, 216 insertions(+), 187 deletions(-) create mode 100644 src/state/game-data.atoms.ts delete mode 100644 src/state/thunks.ts diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index 3fe4fd30a..6ec0bf63b 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -40,6 +40,7 @@ import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useAtomValue } from "jotai"; import { showEligibleCharts } from "../config-state"; import { useAppState } from "../state/store"; +import { gameDataAtom } from "../state/game-data.atoms"; function getAvailableDifficulties(gameData: GameData, selectedStyle: string) { const s = new Set(); @@ -112,7 +113,7 @@ export default function ControlsDrawer() { const dateFormat = "yyyy-MM-dd"; function ReleaseDateFilter() { const { t } = useIntl(); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const updateState = useUpdateConfig(); const cutoffDate = useConfigState((s) => s.cutoffDate); const mostRecentRelease = useMemo( @@ -160,7 +161,7 @@ function ReleaseDateFilter() { function FlagSettings() { const { t } = useIntl(); const dataSetName = useAppState((s) => s.gameData.dataSetName); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const hasFlags = !!gameData?.meta.flags.length; const updateState = useUpdateConfig(); const selectedFlags = useConfigState((s) => s.flags); @@ -197,9 +198,8 @@ function FlagSettings() { /** Renders the checkboxes for each individual folder that exists in the data file's meta.folders */ function FolderSettings() { const { t } = useIntl(); - const availableFolders = useAppState( - (s) => s.gameData.gameData?.meta.folders, - ); + const gameData = useAtomValue(gameDataAtom); + const availableFolders = gameData?.meta.folders; const dataSetName = useAppState((s) => s.gameData.dataSetName); const updateState = useUpdateConfig(); const selectedFolders = useConfigState((s) => s.folders); @@ -254,7 +254,7 @@ function FolderSettings() { function GeneralSettings() { const { t } = useIntl(); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const updateState = useUpdateConfig(); const showingEligibleCharts = useAtomValue(showEligibleCharts); const configState = useConfigState(); diff --git a/src/controls/controls-weights.tsx b/src/controls/controls-weights.tsx index 8c30260f1..9abdfa4cf 100644 --- a/src/controls/controls-weights.tsx +++ b/src/controls/controls-weights.tsx @@ -7,7 +7,8 @@ import { useIntl } from "../hooks/useIntl"; import { NumericInput, Checkbox, Classes } from "@blueprintjs/core"; import { getAvailableLevels } from "../game-data-utils"; import { LevelRangeBucket, getBuckets } from "../card-draw"; -import { useAppState } from "../state/store"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "../state/game-data.atoms"; interface Props { usesTiers: boolean; @@ -52,7 +53,7 @@ export function WeightsControls({ usesTiers, high, low }: Props) { }), shallow, ); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const groups = useMemo(() => { const availableLevels = getAvailableLevels(gameData, useGranularLevels); return Array.from( diff --git a/src/controls/degrs-tester.tsx b/src/controls/degrs-tester.tsx index 753c38640..dc5bd2de7 100644 --- a/src/controls/degrs-tester.tsx +++ b/src/controls/degrs-tester.tsx @@ -6,7 +6,7 @@ import { ProgressBar, } from "@blueprintjs/core"; import { draw } from "../card-draw"; -import { useAtom } from "jotai"; +import { getDefaultStore, useAtom } from "jotai"; import { configSlice } from "../state/config.slice"; import { requestIdleCallback } from "../utils/idle-callback"; import { @@ -20,8 +20,9 @@ import { SongCard, SongCardProps } from "../song-card/song-card"; import { useState } from "react"; import { Rain, Repeat, WarningSign } from "@blueprintjs/icons"; import { EligibleChart, PlayerPickPlaceholder } from "../models/Drawing"; -import { store, useAppStore } from "../state/store"; +import { store } from "../state/store"; import { GameData } from "../models/SongData"; +import { gameDataAtom } from "../state/game-data.atoms"; export function isDegrs(thing: EligibleChart | PlayerPickPlaceholder) { return "name" in thing && thing.name.startsWith('DEAD END("GROOVE'); @@ -63,14 +64,13 @@ export function DegrsTestButton() { const [isTesting, setIsTesting] = useAtom(degrsIsTesting); const [progress, setProgress] = useAtom(degrsTestProgress); const [results, setResults] = useAtom(degrsTestResults); - const store = useAppStore(); async function startTest() { setIsTesting(true); setProgress(0); setResults(undefined); await nextIdleCycle(); - const tester = degrsTester(store.getState().gameData.gameData!); + const tester = degrsTester(getDefaultStore().get(gameDataAtom)!); let report = tester.next(); while (!report.done) { setProgress(report.value); diff --git a/src/controls/index.tsx b/src/controls/index.tsx index b62ad0ff9..dce5cc59e 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -13,14 +13,14 @@ import { NewLayers, Cog } from "@blueprintjs/icons"; import { useState, lazy, Suspense } from "react"; import { FormattedMessage } from "react-intl"; import { useIsNarrow } from "../hooks/useMediaQuery"; -// import { loadConfig, saveConfig } from "../config-persistence"; import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "../utils/error-fallback"; import { ShowChartsToggle } from "./show-charts-toggle"; import { createDraw } from "../state/drawings.slice"; -import { useAppDispatch, useAppState, useAppStore } from "../state/store"; -import { useSetAtom } from "jotai"; +import { useAppDispatch, useAppStore } from "../state/store"; +import { useAtomValue, useSetAtom } from "jotai"; import { showEligibleCharts } from "../config-state"; +import { gameDataLoadingStatus } from "../state/game-data.atoms"; const ControlsDrawer = lazy(() => import("./controls-drawer")); @@ -28,7 +28,7 @@ export function HeaderControls() { const setShowEligibleCharts = useSetAtom(showEligibleCharts); const [settingsOpen, setSettingsOpen] = useState(false); const [lastDrawFailed, setLastDrawFailed] = useState(false); - const hasGameData = useAppState((s) => !!s.gameData.gameData); + const hasGameData = useAtomValue(gameDataLoadingStatus) === "available"; const isNarrow = useIsNarrow(); const dispatch = useAppDispatch(); const store = useAppStore(); diff --git a/src/draw-state.tsx b/src/draw-state.tsx index d66cec711..7a1e27a96 100644 --- a/src/draw-state.tsx +++ b/src/draw-state.tsx @@ -1,36 +1,19 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode } from "react"; import { UnloadHandler } from "./unload-handler"; -import { requestIdleCallback, cancelIdleCallback } from "./utils/idle-callback"; import { I18NDict } from "./models/SongData"; import i18nData from "./assets/i18n.json"; -import { availableGameData, detectedLanguage } from "./utils"; +import { detectedLanguage } from "./utils"; import { IntlProvider } from "./intl-provider"; -import { useAppDispatch, useAppState } from "./state/store"; -import { loadGameDataByName } from "./state/thunks"; +import { useAppState } from "./state/store"; import { drawingsSlice } from "./state/drawings.slice"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "./state/game-data.atoms"; interface Props { defaultDataSet: string; children: ReactNode; } -function getInitialDataSet(defaultDataName: string) { - const hash = window.location.hash.slice(1); - if (hash.startsWith("game-")) { - const targetData = hash.slice(5); - if (availableGameData.some((d) => d.name === targetData)) { - return targetData; - } - } - if ( - defaultDataName && - availableGameData.some((d) => d.name === defaultDataName) - ) { - return defaultDataName; - } - return availableGameData[0].name; -} - // function writeDataSetToUrl(game: string) { // const nextHash = `game-${game}`; // if ("#" + nextHash !== window.location.hash) { @@ -41,15 +24,20 @@ function getInitialDataSet(defaultDataName: string) { // } export function DrawStateManager(props: Props) { - const gameData = useAppState((state) => state.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const hasDrawings = useAppState(drawingsSlice.selectors.haveDrawings); - const dispatch = useAppDispatch(); - useEffect(() => { - const idleHandle = requestIdleCallback(() => - dispatch(loadGameDataByName(getInitialDataSet(props.defaultDataSet))), - ); - return () => cancelIdleCallback(idleHandle); - }, [dispatch, props.defaultDataSet]); + // const dispatch = useAppDispatch(); + // useEffect(() => { + // const idleHandle = requestIdleCallback(() => + // dispatch( + // gameDataSlice.actions.selectGameData({ + // dataSetName: getInitialDataSet(props.defaultDataSet), + // dataType: "stock", + // }), + // ), + // ); + // return () => cancelIdleCallback(idleHandle); + // }, [dispatch, props.defaultDataSet]); return ( { + return { + ...prev, + [parsedPack.name]: derivedData, + }; + }); dispatch( - gameDataSlice.actions.selectCustomData({ - name: parsedPack.name, - gameData: derivedData, + gameDataSlice.actions.selectGameData({ + dataSetName: parsedPack.name, + dataType: "custom", }), ); pause(500).then(() => { setSaving(false); onSave(); }); - }, [parsedPack, derivedData, dispatch, onSave]); + }, [parsedPack, derivedData, setCustomData, dispatch, onSave]); const maybeSkeleton = derivedData ? "" : Classes.SKELETON; diff --git a/src/eligible-charts/histogram.tsx b/src/eligible-charts/histogram.tsx index 2f1025c16..aecc6d77a 100644 --- a/src/eligible-charts/histogram.tsx +++ b/src/eligible-charts/histogram.tsx @@ -19,7 +19,8 @@ import { import { Theme, useTheme } from "../theme-toggle"; import { useIsNarrow } from "../hooks/useMediaQuery"; import { useConfigState } from "../state/hooks"; -import { useAppState } from "../state/store"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "../state/game-data.atoms"; interface Props { charts: EligibleChart[]; @@ -29,7 +30,7 @@ export function DiffHistogram({ charts }: Props) { const { t } = useIntl(); const fgColor = useTheme() === Theme.Dark ? "white" : undefined; const isNarrow = useIsNarrow(); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const allDiffs = gameData?.meta.difficulties; const useGranularLevels = useConfigState((s) => s.useGranularLevels); const availableLevels = getAvailableLevels(gameData, useGranularLevels); diff --git a/src/eligible-charts/index.tsx b/src/eligible-charts/index.tsx index 64d9f6801..4c91e0455 100644 --- a/src/eligible-charts/index.tsx +++ b/src/eligible-charts/index.tsx @@ -11,21 +11,21 @@ import { Button, } from "@blueprintjs/core"; import { useIsNarrow } from "../hooks/useMediaQuery"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { useCallback, useDeferredValue, useMemo } from "react"; import { currentTabAtom, EligibleChartsListFilter } from "./filter"; import { DiffHistogram } from "./histogram"; import { isDegrs, TesterCard } from "../controls/degrs-tester"; import { Export } from "@blueprintjs/icons"; import { shareCharts } from "../utils/share"; -import { useAppState } from "../state/store"; +import { gameDataAtom } from "../state/game-data.atoms"; function songKeyFromChart(chart: EligibleChart) { return `${chart.name}:${chart.artist}`; } export default function EligibleChartsList() { - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); const [currentTab] = useDeferredValue(useAtom(currentTabAtom)); const configState = useDeferredValue(useConfigState()); const isNarrow = useIsNarrow(); diff --git a/src/hooks/useDataSets.ts b/src/hooks/useDataSets.ts index 1a00e783b..c77c4e07d 100644 --- a/src/hooks/useDataSets.ts +++ b/src/hooks/useDataSets.ts @@ -2,33 +2,34 @@ import { useCallback, useMemo } from "react"; import { availableGameData } from "../utils"; import { useAppDispatch, useAppState } from "../state/store"; import { gameDataSlice } from "../state/game-data.slice"; -import { GameData } from "../models/SongData"; -import { loadGameDataByName } from "../state/thunks"; +import { + customDataCache, + gameDataLoadingStatus, +} from "../state/game-data.atoms"; +import { useAtomValue } from "jotai"; export function useDataSets() { const dataSetName = useAppState((s) => s.gameData.dataSetName); const dispatch = useAppDispatch(); - const dataIsLoaded = useAppState((s) => !!s.gameData); - const importedData = useAppState((s) => s.gameData.uploadCache); + const importedData = useAtomValue(customDataCache); + const dataLoadingState = useAtomValue(gameDataLoadingStatus); const loadGameData = useCallback( - (name: string, gameData?: GameData) => { - if (gameData) { + (name: string) => { + if (importedData[name]) { dispatch( - gameDataSlice.actions.selectCustomData({ - name, - gameData, + gameDataSlice.actions.selectGameData({ + dataSetName: name, + dataType: "custom", }), ); - } else if (importedData[name]) { + } else { dispatch( - gameDataSlice.actions.selectCustomData({ - name, - gameData: importedData[name], + gameDataSlice.actions.selectGameData({ + dataSetName: name, + dataType: "stock", }), ); - } else { - dispatch(loadGameDataByName(name)); } }, [dispatch, importedData], @@ -45,12 +46,14 @@ export function useDataSets() { ]; }, [importedData]); - const current = available.find((s) => s.name === dataSetName) || available[0]; + const current: (typeof availableGameData)[0] = available.find( + (s) => s.name === dataSetName, + ) || { display: "Select game data", name: "", parent: "" }; return { available, current, loadData: loadGameData, - dataIsLoaded, + dataIsLoaded: dataLoadingState === "available", }; } diff --git a/src/hooks/useFuzzySearch.ts b/src/hooks/useFuzzySearch.ts index 95a0313fe..250efef33 100644 --- a/src/hooks/useFuzzySearch.ts +++ b/src/hooks/useFuzzySearch.ts @@ -1,8 +1,9 @@ import FuzzySearch from "fuzzy-search"; -import { useAppState } from "../state/store"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "../state/game-data.atoms"; export function useFuzzySearch() { - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); if (!gameData) return null; return new FuzzySearch( gameData.songs, diff --git a/src/last-update.tsx b/src/last-update.tsx index 4d6f7bf16..e3ed638ec 100644 --- a/src/last-update.tsx +++ b/src/last-update.tsx @@ -3,10 +3,12 @@ import cn from "classnames"; import { FormattedMessage } from "react-intl"; import { detectedLanguage } from "./utils"; import { useAppState } from "./state/store"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "./state/game-data.atoms"; export function LastUpdate() { const dataSetName = useAppState((s) => s.gameData.dataSetName); - const gameData = useAppState((s) => s.gameData.gameData); + const gameData = useAtomValue(gameDataAtom); if (!gameData) { return null; } diff --git a/src/party/client.ts b/src/party/client.ts index bfcc7f1fa..3e52326e9 100644 --- a/src/party/client.ts +++ b/src/party/client.ts @@ -4,7 +4,6 @@ import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; import { useEffect } from "react"; -import { loadGameDataByName } from "../state/thunks"; export function PartySocketManager(props: { roomName?: string }) { const dispatch = useAppDispatch(); @@ -44,10 +43,6 @@ export function PartySocketManager(props: { roomName?: string }) { return false; } - // don't try sending the loaded game data through - if (action.type.startsWith(loadGameDataByName.typePrefix)) { - return false; - } return true; }, effect(action) { diff --git a/src/song-search/index.tsx b/src/song-search/index.tsx index ec3e58a96..88b9c916b 100644 --- a/src/song-search/index.tsx +++ b/src/song-search/index.tsx @@ -6,8 +6,9 @@ import { Song } from "../models/SongData"; import { SearchResult, SearchResultData } from "./search-result"; import { Omnibar } from "@blueprintjs/select"; import styles from "./song-search.css"; -import { useAppStore } from "../state/store"; import { useFuzzySearch } from "../hooks/useFuzzySearch"; +import { getDefaultStore } from "jotai"; +import { gameDataAtom } from "../state/game-data.atoms"; interface Props { isOpen: boolean; @@ -20,7 +21,6 @@ export function SongSearch(props: Props) { const [searchTerm, updateSearchTerm] = useState(""); const config = useConfigState(); const fuzzySearch = useFuzzySearch(); - const store = useAppStore(); let items: SearchResultData[] = []; if (fuzzySearch) { @@ -54,7 +54,7 @@ export function SongSearch(props: Props) { item.chart === "none" || !item.chart ? undefined : getDrawnChart( - store.getState().gameData.gameData!, + getDefaultStore().get(gameDataAtom)!, item.song, item.chart, ), diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index 1ce348945..65e075517 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -1,54 +1,46 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { ConfigState, initialState } from "../config-state"; -import { loadGameDataByName } from "./thunks"; import { GameData } from "../models/SongData"; -import { gameDataSlice } from "./game-data.slice"; import { addPlayerNameToDrawing } from "./central"; -function createPartialFromDefaults(gameData: GameData) { - const { lowerLvlBound, upperLvlBound, flags, difficulties, folders, style } = - gameData.defaults; - const ret: Partial = { - lowerBound: lowerLvlBound, - upperBound: upperLvlBound, - flags, - difficulties, - folders, - style, - cutoffDate: "", - }; - if (!gameData.meta.granularTierResolution) { - ret.useGranularLevels = false; - } - return ret; -} - export const configSlice = createSlice({ name: "config", initialState, reducers: { - update: (state, action: PayloadAction>) => { + update(state, action: PayloadAction>) { Object.assign(state, action.payload); }, + applyDefaults( + state, + action: PayloadAction< + GameData["defaults"] & { supportsGranular: boolean } + >, + ) { + const { flags, difficulties, folders, style } = action.payload; + const patch: Partial = { + lowerBound: action.payload.lowerLvlBound, + upperBound: action.payload.upperLvlBound, + flags, + difficulties, + folders, + style, + cutoffDate: "", + }; + if (!action.payload.supportsGranular) { + patch.useGranularLevels = false; + } + Object.assign(state, patch); + }, }, extraReducers(builder) { - builder - .addCase(loadGameDataByName.fulfilled, (state, action) => { - const patch = createPartialFromDefaults(action.payload); - Object.assign(state, patch); - }) - .addCase(gameDataSlice.actions.selectCustomData, (state, action) => { - const patch = createPartialFromDefaults(action.payload.gameData); - Object.assign(state, patch); - }) - .addCase(addPlayerNameToDrawing, (state, action) => { - if (!action.payload.name) { - return; - } - if (state.playerNames.includes(action.payload.name)) { - return; - } - state.playerNames.push(action.payload.name); - }); + builder.addCase(addPlayerNameToDrawing, (state, action) => { + if (!action.payload.name) { + return; + } + if (state.playerNames.includes(action.payload.name)) { + return; + } + state.playerNames.push(action.payload.name); + }); }, }); diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 811e928a7..48d6e4924 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -12,6 +12,8 @@ import { import { addPlayerNameToDrawing } from "./central"; import { AppState } from "./store"; import { draw } from "../card-draw"; +import { getDefaultStore } from "jotai"; +import { gameDataAtom } from "./game-data.atoms"; export const drawingsAdapter = createEntityAdapter({}); @@ -38,12 +40,14 @@ function trackDraw(count: number | null, game?: string) { * addDrawing action in the default successful case */ export function createDraw(state: AppState) { - if (!state.gameData.gameData) { + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + if (!gameData) { trackDraw(null); return false; } - const drawing = draw(state.gameData.gameData, state.config); + const drawing = draw(gameData, state.config); trackDraw(drawing.charts.length, state.gameData.dataSetName); if (!drawing.charts.length) { return true; @@ -180,7 +184,9 @@ export function createRedrawAll(state: AppState, drawingId: string) { ...state.config, chartCount: drawing.charts.length, }; - const drawResult = draw(state.gameData.gameData!, drawConfig); + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + const drawResult = draw(gameData!, drawConfig); return drawingsSlice.actions.updateOne({ id: drawingId, changes: { @@ -202,7 +208,9 @@ export function createRedrawChart( ...state.config, chartCount: 1, }; - const drawResult = draw(state.gameData.gameData!, drawConfig); + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + const drawResult = draw(gameData!, drawConfig); const chart = drawResult.charts.find((c) => c.type === "DRAWN"); if (!chart) { return; diff --git a/src/state/game-data.atoms.ts b/src/state/game-data.atoms.ts new file mode 100644 index 000000000..8a543c1f7 --- /dev/null +++ b/src/state/game-data.atoms.ts @@ -0,0 +1,68 @@ +import { atom, getDefaultStore } from "jotai"; +import { GameData } from "../models/SongData"; +import { startAppListening } from "./listener-middleware"; +import { configSlice } from "./config.slice"; + +export const gameDataAtom = atom(null); + +export const gameDataLoadingStatus = atom< + "loading" | "failed" | "pending" | "available" +>("pending"); + +export const customDataCache = atom>({}); + +async function loadStockGamedataByName(name: string) { + return ( + await import(/* webpackChunkName: "songData" */ `../songs/${name}.json`) + ).default as GameData; +} + +startAppListening({ + predicate(action, currentState, originalState) { + if (currentState.gameData !== originalState.gameData) { + return true; + } + return false; + }, + async effect(action, api) { + console.debug("running game-data.atoms effect"); + const newState = api.getState().gameData; + if (!newState.dataSetName) { + console.debug("no data selected, not loading anything"); + return; + } + const jotaiStore = getDefaultStore(); + let newData: GameData | undefined; + if (newState.dataType === "stock") { + console.debug("stock data"); + jotaiStore.set(gameDataLoadingStatus, "loading"); + try { + newData = await loadStockGamedataByName(newState.dataSetName); + } catch (e) { + console.error("stock data fetch failed", { cause: e }); + jotaiStore.set(gameDataLoadingStatus, "failed"); + return; + } + } else { + console.debug("custom data"); + const customData = jotaiStore.get(customDataCache); + if (customData[newState.dataSetName]) { + newData = customData[newState.dataSetName]; + } + } + if (newData) { + console.log("data arrived", { newData }); + jotaiStore.set(gameDataAtom, newData); + api.dispatch( + configSlice.actions.applyDefaults({ + supportsGranular: !!newData.meta.granularTierResolution, + ...newData.defaults, + }), + ); + jotaiStore.set(gameDataLoadingStatus, "available"); + } else { + console.log("data did not arrive"); + jotaiStore.set(gameDataLoadingStatus, "failed"); + } + }, +}); diff --git a/src/state/game-data.slice.ts b/src/state/game-data.slice.ts index f1db9d176..f83337aa5 100644 --- a/src/state/game-data.slice.ts +++ b/src/state/game-data.slice.ts @@ -1,44 +1,21 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { GameData } from "../models/SongData"; -import { loadGameDataByName } from "./thunks"; interface GameDataState { - gameData: GameData | null; dataSetName: string; - uploadCache: Record; + dataType: "stock" | "custom"; } const initialState: GameDataState = { - gameData: null, dataSetName: "", - uploadCache: {}, + dataType: "stock", }; export const gameDataSlice = createSlice({ name: "gameData", initialState, reducers: { - selectCustomData( - state, - action: PayloadAction<{ name: string; gameData: GameData }>, - ) { - state.dataSetName = action.payload.name; - state.gameData = action.payload.gameData; - if (state.uploadCache[action.payload.name] !== action.payload.gameData) { - state.uploadCache[action.payload.name] = action.payload.gameData; - } + selectGameData(state, action: PayloadAction) { + return action.payload; }, }, - extraReducers(builder) { - builder - .addCase(loadGameDataByName.fulfilled, (state, action) => { - state.gameData = action.payload; - }) - .addCase(loadGameDataByName.pending, (state, action) => { - state.dataSetName = action.meta.arg; - state.gameData = null; - }); - }, }); - -export const { actions } = gameDataSlice; diff --git a/src/state/thunks.ts b/src/state/thunks.ts deleted file mode 100644 index a46f07563..000000000 --- a/src/state/thunks.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { GameData } from "../models/SongData"; -import { AppState, AppDispatch } from "./store"; - -const createTypedThunk = createAsyncThunk.withTypes<{ - state: AppState; - dispatch: AppDispatch; -}>(); - -export const loadGameDataByName = createTypedThunk( - "gameData/load", - async (name: string) => { - return ( - await import(/* webpackChunkName: "songData" */ `../songs/${name}.json`) - ).default as GameData; - }, - { - condition(arg, api) { - const state = api.getState(); - if (arg === state.gameData.dataSetName) { - return false; - } - }, - }, -); diff --git a/src/version-select.tsx b/src/version-select.tsx index daf9fd331..896f99990 100644 --- a/src/version-select.tsx +++ b/src/version-select.tsx @@ -5,13 +5,15 @@ import { Spinner, SpinnerSize, } from "@blueprintjs/core"; +import { Error } from "@blueprintjs/icons"; import { Select } from "@blueprintjs/select"; import { ReactNode, useEffect, useState } from "react"; import { useDataSets } from "./hooks/useDataSets"; import { groupGameData } from "./utils"; import { useIntl } from "./hooks/useIntl"; import { DoubleCaretVertical, FolderOpen } from "@blueprintjs/icons"; -import { useAppState } from "./state/store"; +import { useAtomValue } from "jotai"; +import { gameDataLoadingStatus } from "./state/game-data.atoms"; export function VersionSelect() { const { t } = useIntl(); @@ -70,15 +72,22 @@ export function VersionSelect() { } export function DataLoadingSpinner() { - const dataIsLoading = useAppState((s) => !s.gameData.gameData); - if (!dataIsLoading) { - return null; + const loadingStatus = useAtomValue(gameDataLoadingStatus); + if (loadingStatus === "failed") { + return ( + <> + Couldn't load game! + + ); } - return ( - - Loading game... - - ); + if (loadingStatus === "loading") { + return ( + + Loading game... + + ); + } + return null; } interface DelayProps { From 0da3104c941984087062e87f9e4f32c3508edd03 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 17:43:22 -0700 Subject: [PATCH 011/168] add client-side routing, pull room from url --- package.json | 1 + src/app.tsx | 53 ++++++++++++++++++++++++++++++------- src/draw-state.tsx | 52 ------------------------------------ src/intl-provider.tsx | 40 ++++++++++++++-------------- src/party/client.ts | 15 ++++++++--- src/song-card/song-card.tsx | 2 +- webpack.config.js | 2 ++ yarn.lock | 32 ++++++++++++++++++++++ 8 files changed, 111 insertions(+), 86 deletions(-) delete mode 100644 src/draw-state.tsx diff --git a/package.json b/package.json index 954b1e101..1b42fb3e8 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "react-intl": "^6.6.8", "react-redux": "^9.1.2", "react-refresh": "^0.14.2", + "react-router-dom": "^6.24.1", "sanitize-filename": "^1.6.3", "simfile-parser": "^0.7.2", "style-loader": "^4.0.0", diff --git a/src/app.tsx b/src/app.tsx index 390b2f8e7..11f3f5237 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -10,7 +10,7 @@ FocusStyleManager.onlyShowFocusOnTabs(); import { DrawingList } from "./drawing-list"; import { UpdateManager } from "./update-manager"; -import { DrawStateManager } from "./draw-state"; +import { IntlProvider } from "./intl-provider"; import { Header } from "./header"; import { ThemeSyncWidget } from "./theme-toggle"; import { DropHandler } from "./drop-handler"; @@ -18,17 +18,50 @@ import { Provider } from "react-redux"; import { store } from "./state/store"; import { PartySocketManager } from "./party/client"; -export function App() { +import { + createBrowserRouter, + RouterProvider, + useParams, +} from "react-router-dom"; + +const router = createBrowserRouter([ + { + path: "/", + element: ( + <> +

+ You need to pick an event first. How about this one:{" "} + Default Event +

+ + ), + }, + { + path: "e/:roomName", + element: , + }, +]); + +function AppForRoom() { + const params = useParams<"roomName">(); + if (!params.roomName) { + return null; + } return ( - - - - -
- - - + + + + +
+ + + + ); } + +export function App() { + return ; +} diff --git a/src/draw-state.tsx b/src/draw-state.tsx deleted file mode 100644 index 7a1e27a96..000000000 --- a/src/draw-state.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ReactNode } from "react"; -import { UnloadHandler } from "./unload-handler"; -import { I18NDict } from "./models/SongData"; -import i18nData from "./assets/i18n.json"; -import { detectedLanguage } from "./utils"; -import { IntlProvider } from "./intl-provider"; -import { useAppState } from "./state/store"; -import { drawingsSlice } from "./state/drawings.slice"; -import { useAtomValue } from "jotai"; -import { gameDataAtom } from "./state/game-data.atoms"; - -interface Props { - defaultDataSet: string; - children: ReactNode; -} - -// function writeDataSetToUrl(game: string) { -// const nextHash = `game-${game}`; -// if ("#" + nextHash !== window.location.hash) { -// const nextUrl = new URL(window.location.href); -// nextUrl.hash = encodeURIComponent(nextHash); -// window.history.replaceState(undefined, "", nextUrl); -// } -// } - -export function DrawStateManager(props: Props) { - const gameData = useAtomValue(gameDataAtom); - const hasDrawings = useAppState(drawingsSlice.selectors.haveDrawings); - // const dispatch = useAppDispatch(); - // useEffect(() => { - // const idleHandle = requestIdleCallback(() => - // dispatch( - // gameDataSlice.actions.selectGameData({ - // dataSetName: getInitialDataSet(props.defaultDataSet), - // dataType: "stock", - // }), - // ), - // ); - // return () => cancelIdleCallback(idleHandle); - // }, [dispatch, props.defaultDataSet]); - - return ( - } - mergeTranslations={gameData?.i18n} - > - - {props.children} - - ); -} diff --git a/src/intl-provider.tsx b/src/intl-provider.tsx index 58b1623ad..1c3ae767d 100644 --- a/src/intl-provider.tsx +++ b/src/intl-provider.tsx @@ -1,43 +1,43 @@ +import { useAtomValue } from "jotai"; +import { ReactNode, useMemo } from "react"; import { IntlProvider as UpstreamProvider } from "react-intl"; -import { PropsWithChildren, useMemo } from "react"; -import { flattenedKeys } from "./utils"; +import translations from "./assets/i18n.json"; import { I18NDict } from "./models/SongData"; +import { gameDataAtom } from "./state/game-data.atoms"; +import { detectedLanguage, flattenedKeys } from "./utils"; const FALLBACK_LOCALE = "en"; -interface Props { - locale: string; - translations: Record; - mergeTranslations?: Record; -} +const typedTranslations = translations as Record; + +export function IntlProvider({ children }: { children: ReactNode }) { + const gameSpecificTranslations = useAtomValue(gameDataAtom)?.i18n; -export function IntlProvider({ - locale, - translations, - mergeTranslations, - children, -}: PropsWithChildren) { const messages = useMemo(() => { const ret: Record = {}; - for (const [k, v] of flattenedKeys(translations[FALLBACK_LOCALE])) { + for (const [k, v] of flattenedKeys(typedTranslations[FALLBACK_LOCALE])) { ret[k] = v; } - for (const [k, v] of flattenedKeys(translations[locale])) { + for (const [k, v] of flattenedKeys(typedTranslations[detectedLanguage])) { ret[k] = v; } - if (mergeTranslations) { - for (const [k, v] of flattenedKeys(mergeTranslations[FALLBACK_LOCALE])) { + if (gameSpecificTranslations) { + for (const [k, v] of flattenedKeys( + gameSpecificTranslations[FALLBACK_LOCALE], + )) { ret[`meta.${k}`] = v; } - for (const [k, v] of flattenedKeys(mergeTranslations[locale])) { + for (const [k, v] of flattenedKeys( + gameSpecificTranslations[detectedLanguage], + )) { ret[`meta.${k}`] = v; } } return ret; - }, [translations, mergeTranslations, locale]); + }, [gameSpecificTranslations]); return ( - + {children} ); diff --git a/src/party/client.ts b/src/party/client.ts index 3e52326e9..98a5db88a 100644 --- a/src/party/client.ts +++ b/src/party/client.ts @@ -3,10 +3,14 @@ import type { Broadcast, ReduxAction } from "./types"; import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; -import { useEffect } from "react"; +import React, { useEffect, useState } from "react"; -export function PartySocketManager(props: { roomName?: string }) { +export function PartySocketManager(props: { + roomName?: string; + children: React.ReactNode; +}) { const dispatch = useAppDispatch(); + const [ready, setReady] = useState(false); const socket = usePartySocket({ room: props.roomName, host: "localhost:1999", // TODO determine this based on build type, other stuff @@ -16,6 +20,7 @@ export function PartySocketManager(props: { roomName?: string }) { switch (data.type) { case "roomstate": dispatch(receivePartyState(data.state)); + setReady(true); break; case "action": const foreignAction = { @@ -55,5 +60,9 @@ export function PartySocketManager(props: { roomName?: string }) { }); }, [socket]); - return null; + if (!ready) { + // TODO move this into redux state, provide UI + return null; + } + return props.children; } diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index 086b0db64..3170bc892 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -152,7 +152,7 @@ export function SongCard(props: Props) { let jacketBg = {}; if (jacket) { - const prefix = jacket.startsWith("blob:") ? "" : "jackets/"; + const prefix = jacket.startsWith("blob:") ? "" : "/jackets/"; jacketBg = { backgroundImage: `url("${prefix}${jacket}")`, }; diff --git a/webpack.config.js b/webpack.config.js index 8a1cfe1d2..8b268403c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -33,11 +33,13 @@ module.exports = function (env = {}, argv = {}) { static: "./dist", hot: true, host: "0.0.0.0", + historyApiFallback: true, }, entry: "./src/index.tsx", output: { filename: "[name].[chunkhash:5].js", path: resolve(__dirname, "./dist"), + publicPath: "/", }, optimization: { minimize: isProd, diff --git a/yarn.lock b/yarn.lock index cb35d38e5..c054442e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2783,6 +2783,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.17.1": + version: 1.17.1 + resolution: "@remix-run/router@npm:1.17.1" + checksum: 10/5efc598626cd81688ac26e0abd08204b980831ead8cd2c4b8a27e0c169ee4777fc609fa289c093b93efc3a1e335304698c6961276c2309348444ec7209836c83 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -5186,6 +5193,7 @@ __metadata: react-intl: "npm:^6.6.8" react-redux: "npm:^9.1.2" react-refresh: "npm:^0.14.2" + react-router-dom: "npm:^6.24.1" sanitize-filename: "npm:^1.6.3" simfile-parser: "npm:^0.7.2" style-loader: "npm:^4.0.0" @@ -9968,6 +9976,30 @@ __metadata: languageName: node linkType: hard +"react-router-dom@npm:^6.24.1": + version: 6.24.1 + resolution: "react-router-dom@npm:6.24.1" + dependencies: + "@remix-run/router": "npm:1.17.1" + react-router: "npm:6.24.1" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10/98eeeec3d36695b3d6d8000d8373ba3cefa9afc49ee44f646f3b721e8b47a80f5ce3737ad1f752cccf19caf01e370918750a4bc86a06a99e491731b454d5ec55 + languageName: node + linkType: hard + +"react-router@npm:6.24.1": + version: 6.24.1 + resolution: "react-router@npm:6.24.1" + dependencies: + "@remix-run/router": "npm:1.17.1" + peerDependencies: + react: ">=16.8" + checksum: 10/18ac968171dee286a2f067dc8568faf73c759f833e88e09f1b34ff6e9376d1fd5eade8697a86be83093225956b256b398d935ce2f681c1bf711fb3c058c19839 + languageName: node + linkType: hard + "react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" From 6cee1a40a007db3151af13d949b164621770ef65 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 17:57:19 -0700 Subject: [PATCH 012/168] nicer UI while connecting --- src/app.tsx | 8 ++++++-- src/party/{client.ts => client.tsx} | 18 +++++++++++++++--- src/utils/delay-render.tsx | 25 +++++++++++++++++++++++++ src/version-select.tsx | 20 +------------------- 4 files changed, 47 insertions(+), 24 deletions(-) rename src/party/{client.ts => client.tsx} (75%) create mode 100644 src/utils/delay-render.tsx diff --git a/src/app.tsx b/src/app.tsx index 11f3f5237..9fafbde97 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -51,7 +51,6 @@ function AppForRoom() { -
@@ -63,5 +62,10 @@ function AppForRoom() { } export function App() { - return ; + return ( + <> + + + + ); } diff --git a/src/party/client.ts b/src/party/client.tsx similarity index 75% rename from src/party/client.ts rename to src/party/client.tsx index 98a5db88a..019805fda 100644 --- a/src/party/client.ts +++ b/src/party/client.tsx @@ -4,16 +4,19 @@ import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; import React, { useEffect, useState } from "react"; +import { Card, NonIdealState, Spinner } from "@blueprintjs/core"; +import { DelayRender } from "../utils/delay-render"; export function PartySocketManager(props: { roomName?: string; children: React.ReactNode; }) { const dispatch = useAppDispatch(); + // TODO move this state to redux??? const [ready, setReady] = useState(false); const socket = usePartySocket({ room: props.roomName, - host: "localhost:1999", // TODO determine this based on build type, other stuff + host: process.env.NODE_ENV === "development" ? "localhost:1999" : "", onMessage(evt) { try { const data: Broadcast = JSON.parse(evt.data); @@ -61,8 +64,17 @@ export function PartySocketManager(props: { }, [socket]); if (!ready) { - // TODO move this into redux state, provide UI - return null; + return ( +
+ + + } title="Connecting..." /> + + +
+ ); } return props.children; } diff --git a/src/utils/delay-render.tsx b/src/utils/delay-render.tsx new file mode 100644 index 000000000..b377d6aa0 --- /dev/null +++ b/src/utils/delay-render.tsx @@ -0,0 +1,25 @@ +import { ReactNode, useState, useEffect } from "react"; + +interface DelayProps { + delayMs?: number; + children: ReactNode; +} + +/** + * delays rendering children until 200ms has passed. + * helps to hide flickering loading spinners when loads + * are fast + **/ +export function DelayRender(props: DelayProps) { + const [display, setDisplay] = useState(false); + useEffect(() => { + const handle = setTimeout(() => { + setDisplay(true); + }, props.delayMs || 200); + return () => clearTimeout(handle); + }, [props.delayMs]); + if (display) { + return <>{props.children}; + } + return null; +} diff --git a/src/version-select.tsx b/src/version-select.tsx index 896f99990..2c9391c28 100644 --- a/src/version-select.tsx +++ b/src/version-select.tsx @@ -7,13 +7,13 @@ import { } from "@blueprintjs/core"; import { Error } from "@blueprintjs/icons"; import { Select } from "@blueprintjs/select"; -import { ReactNode, useEffect, useState } from "react"; import { useDataSets } from "./hooks/useDataSets"; import { groupGameData } from "./utils"; import { useIntl } from "./hooks/useIntl"; import { DoubleCaretVertical, FolderOpen } from "@blueprintjs/icons"; import { useAtomValue } from "jotai"; import { gameDataLoadingStatus } from "./state/game-data.atoms"; +import { DelayRender } from "./utils/delay-render"; export function VersionSelect() { const { t } = useIntl(); @@ -89,21 +89,3 @@ export function DataLoadingSpinner() { } return null; } - -interface DelayProps { - children: ReactNode; -} - -function DelayRender(props: DelayProps) { - const [display, setDisplay] = useState(false); - useEffect(() => { - const handle = setTimeout(() => { - setDisplay(true); - }, 200); - return () => clearTimeout(handle); - }, []); - if (display) { - return <>{props.children}; - } - return null; -} From 9e05f4e420042134050ecc41c6bffaf874560205 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 17:59:08 -0700 Subject: [PATCH 013/168] add deployed partykit host --- src/party/client.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/party/client.tsx b/src/party/client.tsx index 019805fda..ebef75964 100644 --- a/src/party/client.tsx +++ b/src/party/client.tsx @@ -16,7 +16,10 @@ export function PartySocketManager(props: { const [ready, setReady] = useState(false); const socket = usePartySocket({ room: props.roomName, - host: process.env.NODE_ENV === "development" ? "localhost:1999" : "", + host: + process.env.NODE_ENV === "development" + ? "localhost:1999" + : "ddr-card-draw-party.noahm.partykit.dev", onMessage(evt) { try { const data: Broadcast = JSON.parse(evt.data); From 6ac6102ec328e787688f850a1e00e1ab18c9c816 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 18:01:02 -0700 Subject: [PATCH 014/168] restore behavior of newest drawings at the top --- src/drawing-list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/drawing-list.tsx b/src/drawing-list.tsx index 9de50cf81..7187e4ad5 100644 --- a/src/drawing-list.tsx +++ b/src/drawing-list.tsx @@ -24,9 +24,9 @@ const ScrollableDrawings = memo(() => { const drawingIds = useDeferredValue(useAppState((s) => s.drawings.ids)); return (
- {drawingIds.map((did) => ( - - ))} + {drawingIds + .map((did) => ) + .reverse()}
); }); From a36c85349170d998eeaf8a23f5226ae532926da0 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jul 2024 18:09:59 -0700 Subject: [PATCH 015/168] add vercel.json for client-side routing support --- vercel.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 000000000..1323cdac3 --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} From acb594ca4ddbcfa60b5f2ab2fe740202ad3d4534 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 12 Jul 2024 14:53:21 -0700 Subject: [PATCH 016/168] add event state slice --- src/state/event.slice.ts | 43 +++++++++++++++++++++++++++++++++++++++ src/state/root-reducer.ts | 2 ++ 2 files changed, 45 insertions(+) create mode 100644 src/state/event.slice.ts diff --git a/src/state/event.slice.ts b/src/state/event.slice.ts new file mode 100644 index 000000000..60d7e982a --- /dev/null +++ b/src/state/event.slice.ts @@ -0,0 +1,43 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { nanoid } from "nanoid"; + +interface CabInfo { + /** drawing id if active */ + activeMatch: string | null; + name: string; + id: string; +} + +interface EventState { + eventName: string; + cabs: Record; +} + +const initialState: EventState = { + eventName: "", + cabs: {}, +}; + +export const eventSlice = createSlice({ + name: "event", + initialState, + reducers: { + /** add a cab with its name */ + addCab(state, action: PayloadAction) { + const newCab: CabInfo = { + id: nanoid(5), + name: action.payload, + activeMatch: null, + }; + state.cabs[newCab.id] = newCab; + }, + assignMatchToCab( + state, + action: PayloadAction<{ cabId: string; matchId: string | null }>, + ) { + const cab = state.cabs[action.payload.cabId]; + if (!cab) return; + cab.activeMatch = action.payload.matchId; + }, + }, +}); diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index b0af54bc6..e39238659 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -3,11 +3,13 @@ import { configSlice } from "./config.slice"; import { drawingsSlice } from "./drawings.slice"; import { gameDataSlice } from "./game-data.slice"; import { receivePartyState } from "./central"; +import { eventSlice } from "./event.slice"; const combinedReducer = combineSlices( drawingsSlice, configSlice, gameDataSlice, + eventSlice, ); export const reducer: typeof combinedReducer = (state, action) => { From 1774363391e4bb96f9fa5ce20d655ca16336e609 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 12 Jul 2024 20:07:48 -0700 Subject: [PATCH 017/168] add persisted webpack cache --- webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index 8b268403c..a7f7ac13a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,6 +25,9 @@ module.exports = function (env = {}, argv = {}) { return { target: "web", + cache: { + type: "filesystem", + }, mode: isProd ? "production" : "development", devtool: isProd ? "source-map" : "inline-cheap-module-source-map", devServer: !serve From 7e6eaea9a49ff9396bee70c2edefce96267b9456 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 12 Jul 2024 20:08:12 -0700 Subject: [PATCH 018/168] add basic cab management --- src/app.tsx | 14 ++- src/cab-management.tsx | 133 ++++++++++++++++++++++++ src/config-state.ts | 2 +- src/drawing-list.tsx | 2 +- src/drawn-set.css | 5 + src/drawn-set.tsx | 1 + src/state/event.slice.ts | 10 +- src/tournament-mode/drawing-actions.tsx | 33 +++++- 8 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 src/cab-management.tsx diff --git a/src/app.tsx b/src/app.tsx index 9fafbde97..3b08254fb 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -23,6 +23,7 @@ import { RouterProvider, useParams, } from "react-router-dom"; +import { CabManagement } from "./cab-management"; const router = createBrowserRouter([ { @@ -53,7 +54,18 @@ function AppForRoom() {
- +
+ + +
diff --git a/src/cab-management.tsx b/src/cab-management.tsx new file mode 100644 index 000000000..92cca57ba --- /dev/null +++ b/src/cab-management.tsx @@ -0,0 +1,133 @@ +import { Button, Card, ControlGroup, InputGroup } from "@blueprintjs/core"; +import { useAppDispatch, useAppState } from "./state/store"; +import { useCallback, useState } from "react"; +import { CabInfo, eventSlice } from "./state/event.slice"; +import { Add, Cross, Remove } from "@blueprintjs/icons"; +import { detectedLanguage } from "./utils"; + +export function CabManagement() { + const cabs = Object.values(useAppState((s) => s.event.cabs)); + return ( +
+
+ +
+
+ {cabs.map((cab) => ( + + ))} +
+
+ ); +} + +function AddCabControl() { + const [name, setName] = useState(""); + const dispatch = useAppDispatch(); + const addCab = useCallback(() => { + dispatch(eventSlice.actions.addCab(name)); + setName(""); + }, [dispatch, name]); + return ( +
{ + e.preventDefault(); + addCab(); + }} + > + + setName(e.currentTarget.value)} + placeholder="Cab name" + /> + + +
+ ); +} + +function CabSummary({ cab }: { cab: CabInfo }) { + const dispatch = useAppDispatch(); + const removeCab = useCallback( + () => dispatch(eventSlice.actions.removeCab(cab.id)), + [dispatch, cab.id], + ); + return ( +
+

+ {cab.name}

+ +
+ ); +} + +const listFormatter = new Intl.ListFormat(detectedLanguage, { + style: "short", + type: "unit", +}); + +function CurrentMatch(props: { cab: CabInfo }) { + const dispatch = useAppDispatch(); + const removeCab = useCallback( + () => + dispatch( + eventSlice.actions.assignMatchToCab({ + cabId: props.cab.id, + matchId: null, + }), + ), + [dispatch, props.cab.id], + ); + const drawing = useAppState((s) => { + if (!props.cab.activeMatch) return null; + return s.drawings.entities[props.cab.activeMatch] || null; + }); + + const scrollToDrawing = useCallback(() => { + if (!drawing) { + return; + } + const el = document.getElementById(`drawing:${drawing.id}`); + if (!el) { + return; + } + const priorFocus = document.querySelector( + "[data-focused]", + ) as HTMLElement | null; + if (priorFocus) { + delete priorFocus.dataset.focused; + } + el.scrollIntoView({ behavior: "smooth" }); + el.dataset.focused = ""; + }, [drawing]); + + if (!drawing) { + return

No match

; + } + const filledPlayers = drawing.players.map( + (p, idx) => p || `Player ${idx + 1}`, + ); + return ( + + +
diff --git a/src/drawn-set.tsx b/src/drawn-set.tsx index f1b594cae..1bed3891e 100644 --- a/src/drawn-set.tsx +++ b/src/drawn-set.tsx @@ -32,6 +32,14 @@ function ChartList() { ); } +export function ChartsOnly({ drawingId }: Props) { + return ( + + + + ); +} + function ChartFromContext({ chartId }: { chartId: string }) { const chart = useDrawing((d) => d.charts.find((c) => c.id === chartId)); const veto = useDrawing((d) => d.bans[chartId]); diff --git a/src/obs-sources/cards.tsx b/src/obs-sources/cards.tsx new file mode 100644 index 000000000..9a930e0ce --- /dev/null +++ b/src/obs-sources/cards.tsx @@ -0,0 +1,12 @@ +import { useParams } from "react-router-dom"; +import { ChartsOnly } from "../drawn-set"; +import { useAppState } from "../state/store"; + +export function CabCards() { + const params = useParams<"roomName" | "cabId">(); + const drawingId = useAppState((s) => s.event.cabs[params.cabId!].activeMatch); + if (!drawingId) { + return null; + } + return ; +} diff --git a/src/obs-sources/text.tsx b/src/obs-sources/text.tsx new file mode 100644 index 000000000..3cbc31020 --- /dev/null +++ b/src/obs-sources/text.tsx @@ -0,0 +1,23 @@ +import { useParams } from "react-router-dom"; +import { drawingSelectors } from "../state/drawings.slice"; +import { useAppState } from "../state/store"; + +export function CabTitle() { + const params = useParams<"roomName" | "cabId">(); + const text = useAppState((s) => { + const drawingId = s.event.cabs[params.cabId!].activeMatch; + if (!drawingId) return null; + return drawingSelectors.selectById(s, drawingId).title; + }); + return

{text}

; +} + +export function CabPlayer(props: { p: number }) { + const params = useParams<"roomName" | "cabId">(); + const text = useAppState((s) => { + const drawingId = s.event.cabs[params.cabId!].activeMatch; + if (!drawingId) return null; + return drawingSelectors.selectById(s, drawingId).players[props.p - 1]; + }); + return

{text}

; +} diff --git a/src/utils/share.ts b/src/utils/share.ts index 170ba9633..9c5454318 100644 --- a/src/utils/share.ts +++ b/src/utils/share.ts @@ -186,6 +186,22 @@ export function copyToClipboard(blob: Blob) { ]); } +export async function copyPlainTextToClipboard( + text: string, + toastMessage?: string, +) { + await navigator.clipboard.writeText(text); + if (toastMessage) { + toaster.show( + { + message: toastMessage, + icon: "paperclip", + }, + "copied-data", + ); + } +} + function dataUriToBlob(dataUri: string) { const headerIndex = dataUri.indexOf(","); const header = dataUri.slice(0, headerIndex); From 2acb83b4de789b71b965d45f0b10ee2b3c160236 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 13 Jul 2024 15:34:41 -0700 Subject: [PATCH 020/168] allow new slices to retain their default state --- src/state/root-reducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index e39238659..d0ffe07a6 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -14,7 +14,7 @@ const combinedReducer = combineSlices( export const reducer: typeof combinedReducer = (state, action) => { if (receivePartyState.match(action)) { - return action.payload; + return Object.assign({}, state, action.payload); } return combinedReducer(state, action); }; From 326c40bac34b71295a7ea1f45660dffe898c0b30 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 13 Jul 2024 15:36:53 -0700 Subject: [PATCH 021/168] prepopulate default cab --- src/state/event.slice.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/state/event.slice.ts b/src/state/event.slice.ts index 9c9cab44e..97a07b4e8 100644 --- a/src/state/event.slice.ts +++ b/src/state/event.slice.ts @@ -18,6 +18,13 @@ const initialState: EventState = { cabs: {}, }; +const defaultCab: CabInfo = { + activeMatch: null, + name: "Primary Cab", + id: nanoid(5), +}; +initialState.cabs[defaultCab.id] = defaultCab; + export const eventSlice = createSlice({ name: "event", initialState, From 07a51097e6eb6af182af23a4788a5076baa3a5de Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 13 Jul 2024 15:42:09 -0700 Subject: [PATCH 022/168] create default cab via lazy initialization --- src/state/event.slice.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/state/event.slice.ts b/src/state/event.slice.ts index 97a07b4e8..73aa715dc 100644 --- a/src/state/event.slice.ts +++ b/src/state/event.slice.ts @@ -13,17 +13,19 @@ interface EventState { cabs: Record; } -const initialState: EventState = { - eventName: "", - cabs: {}, -}; - -const defaultCab: CabInfo = { - activeMatch: null, - name: "Primary Cab", - id: nanoid(5), -}; -initialState.cabs[defaultCab.id] = defaultCab; +function initialState(): EventState { + const defaultCab: CabInfo = { + activeMatch: null, + name: "Primary Cab", + id: nanoid(5), + }; + return { + eventName: "", + cabs: { + [defaultCab.id]: defaultCab, + }, + }; +} export const eventSlice = createSlice({ name: "event", From a9682541799bdc47796579e38de1b84b9f0fd5b6 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 13 Jul 2024 15:50:32 -0700 Subject: [PATCH 023/168] non random initial state --- src/party/server.ts | 17 +++++------------ src/state/event.slice.ts | 11 +++++------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/party/server.ts b/src/party/server.ts index f57281859..028384087 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -3,18 +3,19 @@ import type { ReduxAction, Roomstate } from "./types"; import { configureStore } from "@reduxjs/toolkit"; import { reducer } from "../state/root-reducer"; import { AppState } from "../state/store"; -import { receivePartyState } from "../state/central"; export default class Server implements Party.Server { - private store = configureStore({ reducer }); + // @ts-expect-error I assign this for sure + private store: ReturnType; constructor(readonly room: Party.Room) { console.log("constructor start"); } async onStart() { - console.log("onStart", typeof reducer); - await this.restoreFromStorage(); + const preloadedState = + await this.room.storage.get("currentState"); + this.store = configureStore({ reducer, preloadedState }); this.store.subscribe(() => { this.room.storage.put("currentState", this.store.getState()); }); @@ -53,14 +54,6 @@ export default class Server implements Party.Server { state: this.store.getState(), }); } - - private async restoreFromStorage() { - const preloadedState = - await this.room.storage.get("currentState"); - if (preloadedState) { - this.store.dispatch(receivePartyState(preloadedState)); - } - } } Server satisfies Party.Worker; diff --git a/src/state/event.slice.ts b/src/state/event.slice.ts index 73aa715dc..1f2601d46 100644 --- a/src/state/event.slice.ts +++ b/src/state/event.slice.ts @@ -14,15 +14,14 @@ interface EventState { } function initialState(): EventState { - const defaultCab: CabInfo = { - activeMatch: null, - name: "Primary Cab", - id: nanoid(5), - }; return { eventName: "", cabs: { - [defaultCab.id]: defaultCab, + default: { + id: "default", + name: "Primary Cab", + activeMatch: null, + }, }, }; } From 566d29195fd5ef8a8b5855a46a03545b0f8671dc Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sun, 14 Jul 2024 10:59:52 -0700 Subject: [PATCH 024/168] add very basic gql+codegen setup --- .env.template | 5 + .gitignore | 1 + codegen.ts | 14 + graphql.config.ts | 14 + package.json | 5 + src/startgg-gql/generated/fragment-masking.ts | 87 + src/startgg-gql/generated/gql.ts | 42 + src/startgg-gql/generated/graphql.ts | 2413 ++++++++++++++++ src/startgg-gql/generated/index.ts | 2 + src/startgg-gql/index.ts | 16 + src/startgg-gql/queries.ts | 10 + webpack.config.js | 2 + yarn.lock | 2433 ++++++++++++++++- 13 files changed, 4899 insertions(+), 145 deletions(-) create mode 100644 .env.template create mode 100644 codegen.ts create mode 100644 graphql.config.ts create mode 100644 src/startgg-gql/generated/fragment-masking.ts create mode 100644 src/startgg-gql/generated/gql.ts create mode 100644 src/startgg-gql/generated/graphql.ts create mode 100644 src/startgg-gql/generated/index.ts create mode 100644 src/startgg-gql/index.ts create mode 100644 src/startgg-gql/queries.ts diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..a580f6777 --- /dev/null +++ b/.env.template @@ -0,0 +1,5 @@ +# Generate one of your own for free at +# https://start.gg/admin/profile/developer +# Read more about start.gg auth here: +# https://developer.start.gg/docs/authentication +STARTGG_TOKEN=YOUR_TOKEN_HERE diff --git a/.gitignore b/.gitignore index 6210c7177..d4e36307b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !.yarn/releases !.yarn/sdks !.yarn/versions +.env .partykit dist diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 000000000..490e23ec6 --- /dev/null +++ b/codegen.ts @@ -0,0 +1,14 @@ +import { CodegenConfig } from "@graphql-codegen/cli"; +import baseConfig from "./graphql.config"; + +const config: CodegenConfig = { + ...baseConfig, + ignoreNoDocuments: false, // for better experience with the watcher + generates: { + "./src/startgg-gql/generated/": { + preset: "client", + }, + }, +}; + +export default config; diff --git a/graphql.config.ts b/graphql.config.ts new file mode 100644 index 000000000..56375f723 --- /dev/null +++ b/graphql.config.ts @@ -0,0 +1,14 @@ +import "dotenv/config"; + +export default { + schema: [ + { + "https://api.start.gg/gql/alpha": { + headers: { + Authorization: `Bearer ${process.env.STARTGG_TOKEN}`, + }, + }, + }, + ], + documents: ["./src/startgg-gql/queries.ts"], +}; diff --git a/package.json b/package.json index aa992e9b1..ce34e3fc3 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "@blueprintjs/datetime2": "^2.3.5", "@blueprintjs/icons": "^5.1.0", "@blueprintjs/select": "^5.1.5", + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/client-preset": "^4.3.2", "@lcdp/offline-plugin": "^5.0.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@reduxjs/toolkit": "^2.2.5", @@ -57,6 +59,7 @@ "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.13.0", "@typescript-eslint/parser": "^7.13.0", + "@urql/core": "^5.0.4", "autoprefixer": "^10.4.19", "axios": "^1.7.2", "babel-loader": "^9.1.3", @@ -69,6 +72,7 @@ "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "date-fns": "^2.28.0", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -77,6 +81,7 @@ "favicons-webpack-plugin": "^6.0.0", "fork-ts-checker-webpack-plugin": "^9.0.2", "fuzzy-search": "^3.0.2", + "graphql": "^16.9.0", "he": "^1.2.0", "html-entities": "^2.5.2", "html-loader": "^5.0.0", diff --git a/src/startgg-gql/generated/fragment-masking.ts b/src/startgg-gql/generated/fragment-masking.ts new file mode 100644 index 000000000..aca71b135 --- /dev/null +++ b/src/startgg-gql/generated/fragment-masking.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; + + +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< + infer TType, + any +> + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never + : never + : never; + +// return non-nullable if `fragmentType` is non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> +): TType; +// return nullable if `fragmentType` is undefined +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | undefined +): TType | undefined; +// return nullable if `fragmentType` is nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null +): TType | null; +// return nullable if `fragmentType` is nullable or undefined +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined +): TType | null | undefined; +// return array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> +): Array; +// return array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: Array>> | null | undefined +): Array | null | undefined; +// return readonly array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> +): ReadonlyArray; +// return readonly array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined; +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined +): TType | Array | ReadonlyArray | null | undefined { + return fragmentType as any; +} + + +export function makeFragmentData< + F extends DocumentTypeDecoration, + FT extends ResultOf +>(data: FT, _fragment: F): FragmentType { + return data as FragmentType; +} +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = (fragName && deferredFields[fragName]) || []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/src/startgg-gql/generated/gql.ts b/src/startgg-gql/generated/gql.ts new file mode 100644 index 000000000..bf3542bfc --- /dev/null +++ b/src/startgg-gql/generated/gql.ts @@ -0,0 +1,42 @@ +/* eslint-disable */ +import * as types from './graphql'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ +const documents = { + "\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n": types.EventEntrantsDocument, +}; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function graphql(source: string): unknown; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n"): (typeof documents)["\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n"]; + +export function graphql(source: string) { + return (documents as any)[source] ?? {}; +} + +export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/src/startgg-gql/generated/graphql.ts b/src/startgg-gql/generated/graphql.ts new file mode 100644 index 000000000..e77ac7332 --- /dev/null +++ b/src/startgg-gql/generated/graphql.ts @@ -0,0 +1,2413 @@ +/* eslint-disable */ +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + /** + * The `JSON` scalar type represents JSON values as specified by + * [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). + */ + JSON: { input: any; output: any; } + /** + * Represents a Unix Timestamp. Supports up to 53 bit int values, + * as that is JavaScript's internal memory allocation for integer values. + */ + Timestamp: { input: any; output: any; } +}; + +/** A set of actions available for an entity to take */ +export type ActionSet = { + id?: Maybe; +}; + +/** Represents the state of an activity */ +export enum ActivityState { + /** Activity is active or in progress */ + Active = 'ACTIVE', + /** Activity, like a set, has been called to start */ + Called = 'CALLED', + /** Activity is done */ + Completed = 'COMPLETED', + /** Activity is created */ + Created = 'CREATED', + /** Activity is invalid */ + Invalid = 'INVALID', + /** Activity is queued to run */ + Queued = 'QUEUED', + /** Activity is ready to be started */ + Ready = 'READY' +} + +/** A user's address */ +export type Address = { + __typename?: 'Address'; + city?: Maybe; + country?: Maybe; + countryId?: Maybe; + id?: Maybe; + state?: Maybe; + stateId?: Maybe; +}; + +/** Represents the name of the third-party service (e.g Twitter) for OAuth */ +export enum AuthorizationType { + Battlenet = 'BATTLENET', + Discord = 'DISCORD', + Epic = 'EPIC', + Mixer = 'MIXER', + Steam = 'STEAM', + Twitch = 'TWITCH', + Twitter = 'TWITTER', + Xbox = 'XBOX' +} + +/** Bracket-specific configuration */ +export type BracketConfig = { + bracketType?: Maybe; + id?: Maybe; +}; + +/** Game specific H2H set data such as character, stage, and stock info */ +export type BracketSetGameDataInput = { + /** Score for entrant 1 (if applicable). For smash, this is stocks remaining. */ + entrant1Score?: InputMaybe; + /** Score for entrant 2 (if applicable). For smash, this is stocks remaining. */ + entrant2Score?: InputMaybe; + /** Game number */ + gameNum: Scalars['Int']['input']; + /** List of selections for the game, typically character selections. */ + selections?: InputMaybe>>; + /** ID of the stage that was played for this game (if applicable) */ + stageId?: InputMaybe; + /** Entrant ID of game winner */ + winnerId?: InputMaybe; +}; + +/** Game specific H2H selections made by the entrants, such as character info */ +export type BracketSetGameSelectionInput = { + /** Character selected by this entrant for this game. */ + characterId?: InputMaybe; + /** Entrant ID that made selection */ + entrantId: Scalars['ID']['input']; +}; + +/** The type of Bracket format that a Phase is configured with. */ +export enum BracketType { + Circuit = 'CIRCUIT', + CustomSchedule = 'CUSTOM_SCHEDULE', + DoubleElimination = 'DOUBLE_ELIMINATION', + EliminationRounds = 'ELIMINATION_ROUNDS', + Exhibition = 'EXHIBITION', + Matchmaking = 'MATCHMAKING', + Race = 'RACE', + RoundRobin = 'ROUND_ROBIN', + SingleElimination = 'SINGLE_ELIMINATION', + Swiss = 'SWISS' +} + +/** A character in a videogame */ +export type Character = { + __typename?: 'Character'; + id?: Maybe; + images?: Maybe>>; + /** Name of Character */ + name?: Maybe; +}; + + +/** A character in a videogame */ +export type CharacterImagesArgs = { + type?: InputMaybe; +}; + +/** Comparison operator */ +export enum Comparator { + Equal = 'EQUAL', + GreaterThan = 'GREATER_THAN', + GreaterThanOrEqual = 'GREATER_THAN_OR_EQUAL', + LessThan = 'LESS_THAN', + LessThanOrEqual = 'LESS_THAN_OR_EQUAL' +} + +/** Name, address, etc */ +export type ContactInfo = { + __typename?: 'ContactInfo'; + /** Participant City Name */ + city?: Maybe; + /** Participant Country Name */ + country?: Maybe; + /** Participant Country (region) id */ + countryId?: Maybe; + id?: Maybe; + name?: Maybe; + /** First Name */ + nameFirst?: Maybe; + /** Last Name */ + nameLast?: Maybe; + /** Participant State Name */ + state?: Maybe; + /** Participant State (region) id */ + stateId?: Maybe; + /** Zip or Postal Code */ + zipcode?: Maybe; +}; + +/** An entrant in an event */ +export type Entrant = { + __typename?: 'Entrant'; + event?: Maybe; + id?: Maybe; + /** Entrant's seed number in the first phase of the event. */ + initialSeedNum?: Maybe; + isDisqualified?: Maybe; + /** The entrant name as it appears in bracket: gamerTag of the participant or team name */ + name?: Maybe; + /** Paginated sets for this entrant */ + paginatedSets?: Maybe; + participants?: Maybe>>; + seeds?: Maybe>>; + skill?: Maybe; + /** Standing for this entrant given an event. All entrants queried must be in the same event (for now). */ + standing?: Maybe; + /** @deprecated DEPRECATED. Use streams instead, which supports multiple stream types and teams. */ + stream?: Maybe; + streams?: Maybe>>; + /** Team linked to this entrant, if one exists */ + team?: Maybe; +}; + + +/** An entrant in an event */ +export type EntrantPaginatedSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortType?: InputMaybe; +}; + +export type EntrantConnection = { + __typename?: 'EntrantConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** An event in a tournament */ +export type Event = { + __typename?: 'Event'; + /** How long before the event start will the check-in end (in seconds) */ + checkInBuffer?: Maybe; + /** How long the event check-in will last (in seconds) */ + checkInDuration?: Maybe; + /** Whether check-in is enabled for this event */ + checkInEnabled?: Maybe; + /** Rough categorization of event tier, denoting relative importance in the competitive scene */ + competitionTier?: Maybe; + /** When the event was created (unix timestamp) */ + createdAt?: Maybe; + /** Last date attendees are able to create teams for team events */ + deckSubmissionDeadline?: Maybe; + /** + * Maximum number of participants each Entrant can have + * @deprecated Migrate to teamRosterSize + */ + entrantSizeMax?: Maybe; + /** + * Minimum number of participants each Entrant can have + * @deprecated Migrate to teamRosterSize + */ + entrantSizeMin?: Maybe; + /** The entrants that belong to an event, paginated by filter criteria */ + entrants?: Maybe; + /** Whether the event has decks */ + hasDecks?: Maybe; + /** Are player tasks enabled for this event */ + hasTasks?: Maybe; + id?: Maybe; + images?: Maybe>>; + /** Whether the event is an online event or not */ + isOnline?: Maybe; + league?: Maybe; + /** Markdown field for match rules/instructions */ + matchRulesMarkdown?: Maybe; + /** Title of event set by organizer */ + name?: Maybe; + /** Gets the number of entrants in this event */ + numEntrants?: Maybe; + /** The phase groups that belong to an event. */ + phaseGroups?: Maybe>>; + /** The phases that belong to an event. */ + phases?: Maybe>>; + /** TO settings for prizing */ + prizingInfo?: Maybe; + publishing?: Maybe; + /** Markdown field for event rules/instructions */ + rulesMarkdown?: Maybe; + /** Id of the event ruleset */ + rulesetId?: Maybe; + /** + * Settings pulled from the event ruleset, if one exists + * @deprecated Use ruleset + */ + rulesetSettings?: Maybe; + /** Paginated sets for this Event */ + sets?: Maybe; + slug?: Maybe; + /** Paginated list of standings */ + standings?: Maybe; + /** When does this event start? */ + startAt?: Maybe; + /** The state of the Event. */ + state?: Maybe; + /** Paginated stations on this event */ + stations?: Maybe; + /** Last date attendees are able to create teams for team events */ + teamManagementDeadline?: Maybe; + /** If this is a teams event, returns whether or not teams can set custom names */ + teamNameAllowed?: Maybe; + /** Team roster size requirements */ + teamRosterSize?: Maybe; + tournament?: Maybe; + /** The type of the event, whether an entrant will have one participant or multiple */ + type?: Maybe; + /** When the event was last modified (unix timestamp) */ + updatedAt?: Maybe; + /** Whether the event uses the new EventSeeds for seeding */ + useEventSeeds?: Maybe; + /** The entrant (if applicable) for a given user in this event */ + userEntrant?: Maybe; + videogame?: Maybe; + /** The waves being used by the event */ + waves?: Maybe>>; +}; + + +/** An event in a tournament */ +export type EventEntrantsArgs = { + query?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventImagesArgs = { + type?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventPhasesArgs = { + phaseId?: InputMaybe; + state?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortType?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventStandingsArgs = { + query: StandingPaginationQuery; +}; + + +/** An event in a tournament */ +export type EventStationsArgs = { + query?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventUserEntrantArgs = { + userId?: InputMaybe; +}; + + +/** An event in a tournament */ +export type EventWavesArgs = { + phaseId?: InputMaybe; +}; + +export type EventConnection = { + __typename?: 'EventConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type EventEntrantPageQuery = { + filter?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type EventEntrantPageQueryFilter = { + name?: InputMaybe; +}; + +export type EventFilter = { + fantasyEventId?: InputMaybe; + fantasyRosterHash?: InputMaybe; + id?: InputMaybe; + ids?: InputMaybe>>; + published?: InputMaybe; + slug?: InputMaybe; + type?: InputMaybe>>; + videogameId?: InputMaybe>>; +}; + +/** Name and Gamertag of the owner of an event in a league */ +export type EventOwner = { + __typename?: 'EventOwner'; + email?: Maybe; + eventId?: Maybe; + fullName?: Maybe; + gamerTag?: Maybe; +}; + +export type EventOwnerConnection = { + __typename?: 'EventOwnerConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type EventOwnersQuery = { + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** An event-level Team, in the context of some competition */ +export type EventTeam = Team & { + __typename?: 'EventTeam'; + /** Uniquely identifying token for team. Same as the hashed part of the slug */ + discriminator?: Maybe; + /** @deprecated Use the entrant field off the EventTeam type */ + entrant?: Maybe; + /** @deprecated Use the event field off the EventTeam type */ + event?: Maybe; + globalTeam?: Maybe; + id?: Maybe; + images?: Maybe>>; + members?: Maybe>>; + name?: Maybe; +}; + + +/** An event-level Team, in the context of some competition */ +export type EventTeamImagesArgs = { + type?: InputMaybe; +}; + + +/** An event-level Team, in the context of some competition */ +export type EventTeamMembersArgs = { + status?: InputMaybe>>; +}; + +export type EventTeamConnection = { + __typename?: 'EventTeamConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** Used for league application tiers */ +export type EventTier = { + __typename?: 'EventTier'; + id?: Maybe; + /** Name of this tier */ + name?: Maybe; +}; + +/** A game represents a single game within a set. */ +export type Game = { + __typename?: 'Game'; + /** Score of entrant 1. For smash, this is equivalent to stocks remaining. */ + entrant1Score?: Maybe; + /** Score of entrant 2. For smash, this is equivalent to stocks remaining. */ + entrant2Score?: Maybe; + id?: Maybe; + images?: Maybe>>; + orderNum?: Maybe; + /** Selections for this game such as character, etc. */ + selections?: Maybe>>; + /** The stage that this game was played on (if applicable) */ + stage?: Maybe; + state?: Maybe; + winnerId?: Maybe; +}; + + +/** A game represents a single game within a set. */ +export type GameImagesArgs = { + type?: InputMaybe; +}; + +/** A selection for this game. i.e. character/stage selection, etc */ +export type GameSelection = { + __typename?: 'GameSelection'; + /** If this is a character selection, returns the selected character. */ + character?: Maybe; + /** The entrant who this selection is for */ + entrant?: Maybe; + id?: Maybe; + orderNum?: Maybe; + /** + * The participant who this selection is for. This is only populated if there are + * selections for multiple participants of a single entrant + */ + participant?: Maybe; + selectionType?: Maybe; + selectionValue?: Maybe; +}; + +/** The type of selection i.e. is it for a character or something else */ +export enum GameSelectionType { + /** Character selection */ + Character = 'CHARACTER' +} + +/** Global Team */ +export type GlobalTeam = Team & { + __typename?: 'GlobalTeam'; + /** Uniquely identifying token for team. Same as the hashed part of the slug */ + discriminator?: Maybe; + /** @deprecated Use the entrant field off the EventTeam type */ + entrant?: Maybe; + /** @deprecated Use the event field off the EventTeam type */ + event?: Maybe; + eventTeams?: Maybe; + id?: Maybe; + images?: Maybe>>; + /** Leagues-level teams for leagues this team is competing in */ + leagueTeams?: Maybe; + members?: Maybe>>; + name?: Maybe; +}; + + +/** Global Team */ +export type GlobalTeamEventTeamsArgs = { + query?: InputMaybe; +}; + + +/** Global Team */ +export type GlobalTeamImagesArgs = { + type?: InputMaybe; +}; + + +/** Global Team */ +export type GlobalTeamLeagueTeamsArgs = { + query?: InputMaybe; +}; + + +/** Global Team */ +export type GlobalTeamMembersArgs = { + status?: InputMaybe>>; +}; + +/** An image */ +export type Image = { + __typename?: 'Image'; + height?: Maybe; + id?: Maybe; + ratio?: Maybe; + type?: Maybe; + url?: Maybe; + width?: Maybe; +}; + +/** A league */ +export type League = { + __typename?: 'League'; + addrState?: Maybe; + city?: Maybe; + countryCode?: Maybe; + /** When the tournament was created (unix timestamp) */ + createdAt?: Maybe; + currency?: Maybe; + /** When the tournament ends */ + endAt?: Maybe; + entrantCount?: Maybe; + eventOwners?: Maybe; + /** When does event registration close */ + eventRegistrationClosesAt?: Maybe; + /** Paginated list of events in a league */ + events?: Maybe; + /** + * Hacked "progression" into this final event + * @deprecated No longer used + */ + finalEventId?: Maybe; + /** True if tournament has at least one offline event */ + hasOfflineEvents?: Maybe; + hasOnlineEvents?: Maybe; + hashtag?: Maybe; + id?: Maybe; + images?: Maybe>>; + /** True if tournament has at least one online event */ + isOnline?: Maybe; + lat?: Maybe; + links?: Maybe; + lng?: Maybe; + mapsPlaceId?: Maybe; + /** The tournament name */ + name?: Maybe; + /** + * Top X number of people in the standings who progress to final event + * @deprecated No longer used + */ + numProgressingToFinalEvent?: Maybe; + numUniquePlayers?: Maybe; + postalCode?: Maybe; + primaryContact?: Maybe; + primaryContactType?: Maybe; + /** Publishing settings for this tournament */ + publishing?: Maybe; + /** When does registration for the tournament end */ + registrationClosesAt?: Maybe; + rules?: Maybe; + /** The short slug used to form the url */ + shortSlug?: Maybe; + /** Whether standings for this league should be visible */ + showStandings?: Maybe; + slug?: Maybe; + /** Paginated list of standings */ + standings?: Maybe; + /** When the tournament Starts */ + startAt?: Maybe; + /** State of the tournament, can be ActivityState::CREATED, ActivityState::ACTIVE, or ActivityState::COMPLETED */ + state?: Maybe; + /** When is the team creation deadline */ + teamCreationClosesAt?: Maybe; + tiers?: Maybe>>; + /** The timezone of the tournament */ + timezone?: Maybe; + /** The type of tournament from TournamentType */ + tournamentType?: Maybe; + /** When the tournament was last modified (unix timestamp) */ + updatedAt?: Maybe; + /** Build Tournament URL */ + url?: Maybe; + venueAddress?: Maybe; + venueName?: Maybe; + videogames?: Maybe>>; +}; + + +/** A league */ +export type LeagueEventOwnersArgs = { + query?: InputMaybe; +}; + + +/** A league */ +export type LeagueEventsArgs = { + query?: InputMaybe; +}; + + +/** A league */ +export type LeagueImagesArgs = { + type?: InputMaybe; +}; + + +/** A league */ +export type LeagueStandingsArgs = { + query?: InputMaybe; +}; + + +/** A league */ +export type LeagueUrlArgs = { + relative?: InputMaybe; + tab?: InputMaybe; +}; + +export type LeagueConnection = { + __typename?: 'LeagueConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type LeagueEventsFilter = { + leagueEntrantId?: InputMaybe; + pointMappingGroupIds?: InputMaybe>>; + search?: InputMaybe; + tierIds?: InputMaybe>>; + upcoming?: InputMaybe; + userId?: InputMaybe; +}; + +export type LeagueEventsQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type LeaguePageFilter = { + activeShops?: InputMaybe; + afterDate?: InputMaybe; + beforeDate?: InputMaybe; + computedUpdatedAt?: InputMaybe; + hasBannerImages?: InputMaybe; + id?: InputMaybe; + ids?: InputMaybe>>; + isFeatured?: InputMaybe; + name?: InputMaybe; + /** ID of the user that owns this league. */ + ownerId?: InputMaybe; + past?: InputMaybe; + publiclySearchable?: InputMaybe; + published?: InputMaybe; + upcoming?: InputMaybe; + videogameIds?: InputMaybe>>; +}; + +export type LeagueQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sort?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type LocationFilterType = { + city?: InputMaybe; + countryCode?: InputMaybe; + state?: InputMaybe; +}; + +/** Match-level configuration */ +export type MatchConfig = { + bracketType?: Maybe; + id?: Maybe; +}; + +/** Different options available for verifying player-reported match results */ +export enum MatchConfigVerificationMethod { + Any = 'ANY', + Mixer = 'MIXER', + StreamMe = 'STREAM_ME', + Twitch = 'TWITCH', + Youtube = 'YOUTUBE' +} + +export type Mutation = { + __typename?: 'Mutation'; + /** Delete a phase by id */ + deletePhase?: Maybe; + /** Delete a station by id */ + deleteStation?: Maybe; + /** Delete a wave by id */ + deleteWave?: Maybe; + /** Generate tournament registration Token on behalf of user */ + generateRegistrationToken?: Maybe; + /** Update a set to called state */ + markSetCalled?: Maybe; + /** Update a set to called state */ + markSetInProgress?: Maybe; + /** Register for tournament */ + registerForTournament?: Maybe; + /** + * Report set winner or game stats for a H2H bracket set. If winnerId is + * supplied, mark set as complete. gameData parameter will overwrite any existing + * reported game data. + */ + reportBracketSet?: Maybe>>; + /** Resets set to initial state, can affect other sets and phase groups */ + resetSet?: Maybe; + /** Automatically attempt to resolve all schedule conflicts. Returns a list of changed seeds */ + resolveScheduleConflicts?: Maybe>>; + /** Swap two seed ids in a phase */ + swapSeeds?: Maybe>>; + /** + * Update game stats for a H2H bracket set. Set winner cannot be changed with + * this function, use the resetSet mutation instead. + */ + updateBracketSet?: Maybe; + /** Update set of phase groups in a phase */ + updatePhaseGroups?: Maybe>>; + /** Update the seeding for a phase */ + updatePhaseSeeding?: Maybe; + /** Create or update a Phase */ + upsertPhase?: Maybe; + /** Add or update a station by id */ + upsertStation?: Maybe; + /** Add or update a wave by id */ + upsertWave?: Maybe; +}; + + +export type MutationDeletePhaseArgs = { + phaseId: Scalars['ID']['input']; +}; + + +export type MutationDeleteStationArgs = { + stationId: Scalars['ID']['input']; +}; + + +export type MutationDeleteWaveArgs = { + waveId: Scalars['ID']['input']; +}; + + +export type MutationGenerateRegistrationTokenArgs = { + registration: TournamentRegistrationInput; + userId: Scalars['ID']['input']; +}; + + +export type MutationMarkSetCalledArgs = { + setId: Scalars['ID']['input']; +}; + + +export type MutationMarkSetInProgressArgs = { + setId: Scalars['ID']['input']; +}; + + +export type MutationRegisterForTournamentArgs = { + registration?: InputMaybe; + registrationToken?: InputMaybe; +}; + + +export type MutationReportBracketSetArgs = { + gameData?: InputMaybe>>; + isDQ?: InputMaybe; + setId: Scalars['ID']['input']; + winnerId?: InputMaybe; +}; + + +export type MutationResetSetArgs = { + resetDependentSets?: InputMaybe; + setId: Scalars['ID']['input']; +}; + + +export type MutationResolveScheduleConflictsArgs = { + options?: InputMaybe; + tournamentId: Scalars['ID']['input']; +}; + + +export type MutationSwapSeedsArgs = { + phaseId: Scalars['ID']['input']; + seed1Id: Scalars['ID']['input']; + seed2Id: Scalars['ID']['input']; +}; + + +export type MutationUpdateBracketSetArgs = { + gameData?: InputMaybe>>; + isDQ?: InputMaybe; + setId: Scalars['ID']['input']; + winnerId?: InputMaybe; +}; + + +export type MutationUpdatePhaseGroupsArgs = { + groupConfigs: Array>; +}; + + +export type MutationUpdatePhaseSeedingArgs = { + options?: InputMaybe; + phaseId: Scalars['ID']['input']; + seedMapping: Array>; +}; + + +export type MutationUpsertPhaseArgs = { + eventId?: InputMaybe; + payload: PhaseUpsertInput; + phaseId?: InputMaybe; +}; + + +export type MutationUpsertStationArgs = { + fields: StationUpsertInput; + stationId?: InputMaybe; + tournamentId?: InputMaybe; +}; + + +export type MutationUpsertWaveArgs = { + fields: WaveUpsertInput; + tournamentId?: InputMaybe; + waveId?: InputMaybe; +}; + +export type PageInfo = { + __typename?: 'PageInfo'; + filter?: Maybe; + page?: Maybe; + perPage?: Maybe; + sortBy?: Maybe; + total?: Maybe; + totalPages?: Maybe; +}; + +export type PaginationSearchType = { + fieldsToSearch?: InputMaybe>>; + searchString?: InputMaybe; +}; + +/** A participant of a tournament; either a spectator or competitor */ +export type Participant = { + __typename?: 'Participant'; + /** If this participant was checked-in by admin */ + checkedIn?: Maybe; + /** The time this participant was checked-in by admin */ + checkedInAt?: Maybe; + /** Info for connected accounts to external services. */ + connectedAccounts?: Maybe; + /** + * Contact Info selected during registration. Falls back to User.location and/or + * User.name if necessary. These fields are for admin use only. If you are not a + * tournament admin or the participant being queried, these fields will be null. + * Do not display this information publicly. + */ + contactInfo?: Maybe; + /** Email of the user, only available to admins within 18 months of tournament completion for tournament administrators. */ + email?: Maybe; + /** Entrants associated with this Participant, if applicable */ + entrants?: Maybe>>; + /** The events this participant registered for within a Tournament. */ + events?: Maybe>>; + /** The tag that was used when the participant registered, e.g. Mang0 */ + gamerTag?: Maybe; + id?: Maybe; + images?: Maybe>>; + player?: Maybe; + /** The prefix that the user set for this Tournament, e.g. C9 */ + prefix?: Maybe; + /** Tournament Admin viewable field. Shows details for required social connections */ + requiredConnections?: Maybe>>; + /** The user this participant is associated to. */ + user?: Maybe; + /** If this participant is verified as actually being in the tournament */ + verified?: Maybe; +}; + + +/** A participant of a tournament; either a spectator or competitor */ +export type ParticipantImagesArgs = { + type?: InputMaybe; +}; + +export type ParticipantConnection = { + __typename?: 'ParticipantConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type ParticipantPageFilter = { + checkedIn?: InputMaybe; + eventIds?: InputMaybe>>; + gamerTag?: InputMaybe; + id?: InputMaybe; + ids?: InputMaybe>>; + incompleteTeam?: InputMaybe; + missingDeck?: InputMaybe; + notCheckedIn?: InputMaybe; + search?: InputMaybe; + unpaid?: InputMaybe; +}; + +export type ParticipantPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** A phase in an event */ +export type Phase = { + __typename?: 'Phase'; + /** The bracket type of this phase. */ + bracketType?: Maybe; + /** The Event that this phase belongs to */ + event?: Maybe; + /** Number of phase groups in this phase */ + groupCount?: Maybe; + id?: Maybe; + /** Is the phase an exhibition or not. */ + isExhibition?: Maybe; + /** Name of phase e.g. Round 1 Pools */ + name?: Maybe; + /** The number of seeds this phase contains. */ + numSeeds?: Maybe; + /** @deprecated Please use 'seeds' instead */ + paginatedSeeds?: Maybe; + /** Phase groups under this phase, paginated */ + phaseGroups?: Maybe; + /** The relative order of this phase within an event */ + phaseOrder?: Maybe; + /** Paginated seeds for this phase */ + seeds?: Maybe; + /** Paginated sets for this Phase */ + sets?: Maybe; + /** State of the phase */ + state?: Maybe; + waves?: Maybe>>; +}; + + +/** A phase in an event */ +export type PhasePaginatedSeedsArgs = { + eventId?: InputMaybe; + query: SeedPaginationQuery; +}; + + +/** A phase in an event */ +export type PhasePhaseGroupsArgs = { + query?: InputMaybe; +}; + + +/** A phase in an event */ +export type PhaseSeedsArgs = { + eventId?: InputMaybe; + query: SeedPaginationQuery; +}; + + +/** A phase in an event */ +export type PhaseSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortType?: InputMaybe; +}; + +/** A group within a phase */ +export type PhaseGroup = { + __typename?: 'PhaseGroup'; + /** The bracket type of this group's phase. */ + bracketType?: Maybe; + /** URL for this phase groups's bracket. */ + bracketUrl?: Maybe; + /** Unique identifier for this group within the context of its phase */ + displayIdentifier?: Maybe; + /** For the given phase group, this is the start time of the first round that occurs in the group. */ + firstRoundTime?: Maybe; + id?: Maybe; + numRounds?: Maybe; + /** @deprecated Please use 'seeds', which is now paginated */ + paginatedSeeds?: Maybe; + /** + * Paginated sets on this phaseGroup + * @deprecated Please use 'sets', which is now paginated + */ + paginatedSets?: Maybe; + /** The phase associated with this phase group */ + phase?: Maybe; + /** The progressions out of this phase group */ + progressionsOut?: Maybe>>; + rounds?: Maybe>>; + seedMap?: Maybe; + /** Paginated seeds for this phase group */ + seeds?: Maybe; + /** Paginated sets on this phaseGroup */ + sets?: Maybe; + /** Paginated list of standings */ + standings?: Maybe; + /** Unix time the group is scheduled to start. This info could also be on the wave instead. */ + startAt?: Maybe; + state?: Maybe; + tiebreakOrder?: Maybe; + wave?: Maybe; +}; + + +/** A group within a phase */ +export type PhaseGroupPaginatedSeedsArgs = { + eventId?: InputMaybe; + query: SeedPaginationQuery; +}; + + +/** A group within a phase */ +export type PhaseGroupPaginatedSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortType?: InputMaybe; +}; + + +/** A group within a phase */ +export type PhaseGroupSeedsArgs = { + eventId?: InputMaybe; + query: SeedPaginationQuery; +}; + + +/** A group within a phase */ +export type PhaseGroupSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortType?: InputMaybe; +}; + + +/** A group within a phase */ +export type PhaseGroupStandingsArgs = { + query?: InputMaybe; +}; + +export type PhaseGroupConnection = { + __typename?: 'PhaseGroupConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type PhaseGroupPageQuery = { + entrantIds?: InputMaybe>>; + filter?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type PhaseGroupPageQueryFilter = { + id?: InputMaybe>>; + waveId?: InputMaybe; +}; + +export type PhaseGroupUpdateInput = { + phaseGroupId: Scalars['ID']['input']; + stationId?: InputMaybe; + waveId?: InputMaybe; +}; + +export type PhaseUpsertInput = { + bracketType?: InputMaybe; + /** The number of pools to configure for the Phase. Only applies to brackets that support pools */ + groupCount?: InputMaybe; + /** The name of the Phase. For example, "Top 8" or "Pools" */ + name?: InputMaybe; +}; + +/** A player */ +export type Player = { + __typename?: 'Player'; + gamerTag?: Maybe; + id?: Maybe; + prefix?: Maybe; + /** Most recent active & published rankings */ + rankings?: Maybe>>; + /** + * Recent sets for this player. + * @deprecated Use the sets field instead. + */ + recentSets?: Maybe>>; + /** Recent standings */ + recentStandings?: Maybe>>; + /** Set history for this player. */ + sets?: Maybe; + user?: Maybe; +}; + + +/** A player */ +export type PlayerRankingsArgs = { + limit?: InputMaybe; + videogameId?: InputMaybe; +}; + + +/** A player */ +export type PlayerRecentSetsArgs = { + opponentId?: InputMaybe; +}; + + +/** A player */ +export type PlayerRecentStandingsArgs = { + limit?: InputMaybe; + videogameId?: InputMaybe; +}; + + +/** A player */ +export type PlayerSetsArgs = { + filters?: InputMaybe; + page?: InputMaybe; + perPage?: InputMaybe; +}; + +/** A player's ranks */ +export type PlayerRank = { + __typename?: 'PlayerRank'; + id?: Maybe; + /** The player's placement on the ranking */ + rank?: Maybe; + title?: Maybe; +}; + +/** An OAuth ProfileAuthorization object */ +export type ProfileAuthorization = { + __typename?: 'ProfileAuthorization'; + /** The id given by the external service */ + externalId?: Maybe; + /** The username given by the external service (including discriminator if discord) */ + externalUsername?: Maybe; + id?: Maybe; + stream?: Maybe; + /** The name of the external service providing this auth i.e. "twitch" */ + type?: Maybe; + url?: Maybe; +}; + +/** A connection between a placement in an origin phase group to a destination seed. */ +export type Progression = { + __typename?: 'Progression'; + id?: Maybe; + originOrder?: Maybe; + originPhase?: Maybe; + originPhaseGroup?: Maybe; + originPlacement?: Maybe; +}; + +export type Query = { + __typename?: 'Query'; + /** Returns the authenticated user */ + currentUser?: Maybe; + /** Returns an entrant given its id */ + entrant?: Maybe; + /** Returns an event given its id or slug */ + event?: Maybe; + /** Returns a league given its id or slug */ + league?: Maybe; + /** Paginated, filterable list of leagues */ + leagues?: Maybe; + /** Returns a participant given its id */ + participant?: Maybe; + /** Returns a phase given its id */ + phase?: Maybe; + /** Returns a phase group given its id */ + phaseGroup?: Maybe; + /** Returns a player given an id */ + player?: Maybe; + /** Returns a phase seed given its id */ + seed?: Maybe; + /** Returns a set given its id */ + set?: Maybe; + /** A shop entity */ + shop?: Maybe; + /** Returns an stream given its id */ + stream?: Maybe; + /** Returns all the stream queues for a given tournament */ + streamQueue?: Maybe>>; + /** Returns a team given its id */ + team?: Maybe; + /** Returns a tournament given its id or slug */ + tournament?: Maybe; + /** Paginated, filterable list of tournaments */ + tournaments?: Maybe; + /** Returns a user given a user slug of the form user/abc123, or id */ + user?: Maybe; + /** Returns a videogame given its id */ + videogame?: Maybe; + /** Returns paginated list of videogames matching the search criteria. */ + videogames?: Maybe; +}; + + +export type QueryEntrantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryEventArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryLeagueArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryLeaguesArgs = { + query: LeagueQuery; +}; + + +export type QueryParticipantArgs = { + id: Scalars['ID']['input']; + isAdmin?: InputMaybe; +}; + + +export type QueryPhaseArgs = { + id?: InputMaybe; +}; + + +export type QueryPhaseGroupArgs = { + id?: InputMaybe; +}; + + +export type QueryPlayerArgs = { + id: Scalars['ID']['input']; +}; + + +export type QuerySeedArgs = { + id?: InputMaybe; +}; + + +export type QuerySetArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryShopArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryStreamArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryStreamQueueArgs = { + includePlayerStreams?: InputMaybe; + tournamentId: Scalars['ID']['input']; +}; + + +export type QueryTeamArgs = { + id?: InputMaybe; + inviteCode?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryTournamentArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryTournamentsArgs = { + query: TournamentQuery; +}; + + +export type QueryUserArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryVideogameArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryVideogamesArgs = { + query: VideogameQuery; +}; + +/** Race specific bracket configuration */ +export type RaceBracketConfig = BracketConfig & { + __typename?: 'RaceBracketConfig'; + automaticEndTime?: Maybe; + automaticStartTime?: Maybe; + bracketType?: Maybe; + goalTargetComparator?: Maybe; + goalTargetValue?: Maybe; + id?: Maybe; + limitMode?: Maybe; + limitValue?: Maybe; + raceType?: Maybe; +}; + +/** Enforces limits on the amount of allowable Race submissions */ +export enum RaceLimitMode { + BestAll = 'BEST_ALL', + FirstAll = 'FIRST_ALL', + Playtime = 'PLAYTIME' +} + +/** Race specific match configuration */ +export type RaceMatchConfig = MatchConfig & { + __typename?: 'RaceMatchConfig'; + bracketType?: Maybe; + id?: Maybe; + /** Can players report results? */ + playerReportingEnabled?: Maybe; + /** Accepted methods of verification that players can use */ + verificationMethods?: Maybe>>; + /** Are players required to submit verification of their reported results? */ + verificationRequired?: Maybe; +}; + +/** Race type */ +export enum RaceType { + Goals = 'GOALS', + Timed = 'TIMED' +} + +export type ResetAffectedData = { + __typename?: 'ResetAffectedData'; + affectedPhaseGroupCount?: Maybe; + affectedSetCount?: Maybe; + affectedSets?: Maybe>>; +}; + +export type ResolveConflictsLockedSeedConfig = { + eventId: Scalars['ID']['input']; + numSeeds: Scalars['Int']['input']; +}; + +export type ResolveConflictsOptions = { + lockedSeeds?: InputMaybe>>; +}; + +/** A round within a phase group */ +export type Round = { + __typename?: 'Round'; + /** + * If applicable, bestOf is the number of games + * one must win a majority out of to win a set in this round + */ + bestOf?: Maybe; + id?: Maybe; + /** Indicates this round's order in the phase group */ + number?: Maybe; + /** The time that this round is scheduled to start at */ + startAt?: Maybe; +}; + +/** + * The score that led to this standing being awarded. The meaning of this field can + * vary by standing type and is not used for some standing types. + */ +export type Score = { + __typename?: 'Score'; + /** Like value, but formatted for race format events. Formatted according to the race config for the front end to use. */ + displayValue?: Maybe; + /** The name of this score. e.g. "Kills" or "Stocks" */ + label?: Maybe; + /** The raw score value */ + value?: Maybe; +}; + +/** A seed for an entrant */ +export type Seed = { + __typename?: 'Seed'; + /** Map of Participant ID to checked in boolean */ + checkedInParticipants?: Maybe; + entrant?: Maybe; + groupSeedNum?: Maybe; + id?: Maybe; + isBye?: Maybe; + phase?: Maybe; + phaseGroup?: Maybe; + placeholderName?: Maybe; + placement?: Maybe; + /** The player(s) associated with this seed's entrant */ + players?: Maybe>>; + progressionSeedId?: Maybe; + /** Source progression information */ + progressionSource?: Maybe; + seedNum?: Maybe; + /** Entrant's win/loss record for this standing. Scores do not include byes. */ + setRecordWithoutByes?: Maybe; + standings?: Maybe>>; +}; + + +/** A seed for an entrant */ +export type SeedSetRecordWithoutByesArgs = { + phaseGroupId: Scalars['ID']['input']; +}; + + +/** A seed for an entrant */ +export type SeedStandingsArgs = { + containerType?: InputMaybe; +}; + +export type SeedConnection = { + __typename?: 'SeedConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type SeedPageFilter = { + checkInState?: InputMaybe>>; + entrantName?: InputMaybe; + eventCheckInGroupId?: InputMaybe; + eventId?: InputMaybe; + id?: InputMaybe; + phaseGroupId?: InputMaybe>>; + phaseId?: InputMaybe>>; + search?: InputMaybe; +}; + +export type SeedPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** A set */ +export type Set = { + __typename?: 'Set'; + /** The time this set was marked as completed */ + completedAt?: Maybe; + /** The time this set was created */ + createdAt?: Maybe; + displayScore?: Maybe; + /** Event that this set belongs to. */ + event?: Maybe; + /** Full round text of this set. */ + fullRoundText?: Maybe; + game?: Maybe; + games?: Maybe>>; + /** Whether this set contains a placeholder entrant */ + hasPlaceholder?: Maybe; + id?: Maybe; + /** The letters that describe a unique identifier within the pool. Eg. F, AT */ + identifier?: Maybe; + images?: Maybe>>; + lPlacement?: Maybe; + /** Phase group that this Set belongs to. */ + phaseGroup?: Maybe; + /** The sets that are affected from resetting this set */ + resetAffectedData?: Maybe; + /** The round number of the set. Negative numbers are losers bracket */ + round?: Maybe; + /** + * Indicates whether the set is in best of or total games mode. This instructs + * which field is used to figure out how many games are in this set. + */ + setGamesType?: Maybe; + /** A possible spot in a set. Use this to get all entrants in a set. Use this for all bracket types (FFA, elimination, etc) */ + slots?: Maybe>>; + /** The start time of the Set. If there is no startAt time on the Set, will pull it from phaseGroup rounds configuration. */ + startAt?: Maybe; + startedAt?: Maybe; + state?: Maybe; + /** Tournament event station for a set */ + station?: Maybe; + /** Tournament event stream for a set */ + stream?: Maybe; + /** If setGamesType is in total games mode, this defined the number of games in the set. */ + totalGames?: Maybe; + /** Url of a VOD for this set */ + vodUrl?: Maybe; + wPlacement?: Maybe; + winnerId?: Maybe; +}; + + +/** A set */ +export type SetDisplayScoreArgs = { + mainEntrantId?: InputMaybe; +}; + + +/** A set */ +export type SetGameArgs = { + orderNum: Scalars['Int']['input']; +}; + + +/** A set */ +export type SetImagesArgs = { + type?: InputMaybe; +}; + + +/** A set */ +export type SetSlotsArgs = { + includeByes?: InputMaybe; +}; + +export type SetConnection = { + __typename?: 'SetConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** Filter Sets by geographical constraints. */ +export type SetFilterLocation = { + /** Only return Sets in this country. Expects a valid two-letter country code */ + country?: InputMaybe; + distanceFrom?: InputMaybe; + /** Only return Sets in this state. Only applicable to US states */ + state?: InputMaybe; +}; + +/** Only return Sets that are a certain distance away from a specified point */ +export type SetFilterLocationDistanceFrom = { + /** Point at which to perform distance calculation */ + point?: InputMaybe; + /** Distance from the point to include results in */ + radius?: InputMaybe; +}; + +export type SetFilterLocationDistanceFromPoint = { + lat?: InputMaybe; + lon?: InputMaybe; +}; + +export type SetFilters = { + /** Only return Sets for these Entrants */ + entrantIds?: InputMaybe>>; + /** Only return Sets for this Entrant size. For example, to fetch 1v1 Sets only, filter by an entrantSize of 1 */ + entrantSize?: InputMaybe>>; + /** Only return Sets in these Events */ + eventIds?: InputMaybe>>; + /** Only return Sets that have an attached VOD */ + hasVod?: InputMaybe; + /** Do not return empty Sets. For example, set this to true to filter out sets that are waiting for progressions. */ + hideEmpty?: InputMaybe; + /** Only return Sets that are in an Online event. If omitted, Sets for both online and offline Events are returned */ + isEventOnline?: InputMaybe; + /** Only return Sets in certain geographical areas. */ + location?: InputMaybe; + /** Only return Sets for these Participants */ + participantIds?: InputMaybe>>; + /** Only return Sets in these PhaseGroups */ + phaseGroupIds?: InputMaybe>>; + /** Only return Sets in these Phases */ + phaseIds?: InputMaybe>>; + /** Only return Sets for these Players */ + playerIds?: InputMaybe>>; + /** Only return Sets for these Rounds */ + roundNumber?: InputMaybe; + /** Return sets that contain a bye */ + showByes?: InputMaybe; + /** Only returns Sets that are in these states */ + state?: InputMaybe>>; + /** Only return Sets that are assigned to these Station IDs */ + stationIds?: InputMaybe>>; + /** Only return Sets that are assigned to these Station numbers */ + stationNumbers?: InputMaybe>>; + /** Only return Sets in these Tournaments */ + tournamentIds?: InputMaybe>>; + /** Only return sets created or updated since this timestamp */ + updatedAfter?: InputMaybe; +}; + +/** A slot in a set where a seed currently or will eventually exist in order to participate in the set. */ +export type SetSlot = { + __typename?: 'SetSlot'; + entrant?: Maybe; + id?: Maybe; + /** Pairs with prereqType, is the ID of the prereq. */ + prereqId?: Maybe; + /** Given a set prereq type, defines the placement required in the origin set to end up in this slot. */ + prereqPlacement?: Maybe; + /** Describes where the entity in this slot comes from. */ + prereqType?: Maybe; + seed?: Maybe; + /** The index of the slot. Unique per set. */ + slotIndex?: Maybe; + /** The standing within this set for the seed currently assigned to this slot. */ + standing?: Maybe; +}; + +/** Different sort type configurations used when displaying multiple sets */ +export enum SetSortType { + /** Sets are sorted in the suggested order that they be called to be played. The order of completed sets is reversed. */ + CallOrder = 'CALL_ORDER', + /** Sets are sorted by relevancy dependent on the state and progress of the event. */ + Magic = 'MAGIC', + /** Sets will not be sorted. */ + None = 'NONE', + /** Sets are sorted in the order that they were started. */ + Recent = 'RECENT', + /** Sets sorted by round and identifier */ + Round = 'ROUND', + /** Deprecated. This is equivalent to CALL_ORDER */ + Standard = 'STANDARD' +} + +/** A shop */ +export type Shop = { + __typename?: 'Shop'; + id?: Maybe; + levels?: Maybe; + messages?: Maybe; + name?: Maybe; + slug?: Maybe; + url?: Maybe; +}; + + +/** A shop */ +export type ShopLevelsArgs = { + query?: InputMaybe; +}; + + +/** A shop */ +export type ShopMessagesArgs = { + query?: InputMaybe; +}; + +/** A shop level */ +export type ShopLevel = { + __typename?: 'ShopLevel'; + currAmount?: Maybe; + description?: Maybe; + goalAmount?: Maybe; + id?: Maybe; + images?: Maybe>>; + name?: Maybe; +}; + + +/** A shop level */ +export type ShopLevelImagesArgs = { + type?: InputMaybe; +}; + +export type ShopLevelConnection = { + __typename?: 'ShopLevelConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type ShopLevelsQuery = { + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** The message and player info for a shop order */ +export type ShopOrderMessage = { + __typename?: 'ShopOrderMessage'; + /** The player's gamertag. Returns null if anonymous message type */ + gamertag?: Maybe; + id?: Maybe; + /** The order message */ + message?: Maybe; + /** The player's name. Returns null unless name & tag display is selected */ + name?: Maybe; + /** The player who left the comment */ + player?: Maybe; + /** The total order amount */ + total?: Maybe; +}; + +export type ShopOrderMessageConnection = { + __typename?: 'ShopOrderMessageConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type ShopOrderMessagesQuery = { + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** Represents the name of the third-party social service (e.g Twitter) for OAuth */ +export enum SocialConnectionType { + Discord = 'DISCORD', + Mixer = 'MIXER', + Twitch = 'TWITCH', + Twitter = 'TWITTER', + Xbox = 'XBOX' +} + +/** Video Stage */ +export type Stage = { + __typename?: 'Stage'; + id?: Maybe; + /** Stage name */ + name?: Maybe; +}; + +/** A standing indicates the placement of something within a container. */ +export type Standing = { + __typename?: 'Standing'; + /** + * The containing entity that contextualizes this standing. Event standings, for + * example, represent an entrant's standing in the entire event vs. Set standings + * which is an entrant's standing in only a single set within an event. + */ + container?: Maybe; + /** If the entity this standing is assigned to can be resolved into an entrant, this will provide the entrant. */ + entrant?: Maybe; + id?: Maybe; + isFinal?: Maybe; + /** Metadata that goes along with this standing. Can take on different forms based on standing group type and settings. */ + metadata?: Maybe; + placement?: Maybe; + /** The player(s) tied to this standing's entity */ + player?: Maybe; + /** @deprecated The "placement" field is identical and will eventually replace "standing" */ + standing?: Maybe; + stats?: Maybe; + totalPoints?: Maybe; +}; + +export type StandingConnection = { + __typename?: 'StandingConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** The containing entity that this standing is for */ +export type StandingContainer = Event | PhaseGroup | Set | Tournament; + +export type StandingGroupStandingPageFilter = { + page?: InputMaybe; + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type StandingPageFilter = { + id?: InputMaybe; + ids?: InputMaybe>>; + search?: InputMaybe; +}; + +export type StandingPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** Any stats related to this standing. This type is experimental and very likely to change in the future. */ +export type StandingStats = { + __typename?: 'StandingStats'; + score?: Maybe; +}; + +export type StationFilter = { + page?: InputMaybe; + perPage?: InputMaybe; +}; + +export type StationUpsertInput = { + clusterId?: InputMaybe; + number: Scalars['Int']['input']; +}; + +/** Stations, such as a stream setup, at an event */ +export type Stations = { + __typename?: 'Stations'; + canAutoAssign?: Maybe; + clusterNumber?: Maybe; + clusterPrefix?: Maybe; + enabled?: Maybe; + id?: Maybe; + identifier?: Maybe; + numSetups?: Maybe; + number?: Maybe; + prefix?: Maybe; + queue?: Maybe; + queueDepth?: Maybe; + state?: Maybe; + updatedAt?: Maybe; +}; + +export type StationsConnection = { + __typename?: 'StationsConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** A Stream object */ +export type Stream = { + __typename?: 'Stream'; + id?: Maybe; + /** Whether the stream is currently live. May be slightly delayed. */ + isOnline?: Maybe; + /** The name of the stream */ + name?: Maybe; + /** The name of the external service providing this auth i.e. "twitch" */ + type?: Maybe; +}; + +/** A Stream queue object */ +export type StreamQueue = { + __typename?: 'StreamQueue'; + id?: Maybe; + /** The sets on the stream */ + sets?: Maybe>>; + /** The stream on the queue */ + stream?: Maybe; +}; + +/** Represents the source of a stream */ +export enum StreamSource { + /** Stream is on smashcast.tv channel */ + Hitbox = 'HITBOX', + /** Stream is on a mixer.com channel */ + Mixer = 'MIXER', + /** Stream is on a stream.me channel */ + Streamme = 'STREAMME', + /** Stream is on twitch.tv channel */ + Twitch = 'TWITCH', + /** Stream is on a youtube.com channel */ + Youtube = 'YOUTUBE' +} + +/** Represents the type of stream service */ +export enum StreamType { + Mixer = 'MIXER', + Twitch = 'TWITCH', + Youtube = 'YOUTUBE' +} + +/** Tournament Stream */ +export type Streams = { + __typename?: 'Streams'; + enabled?: Maybe; + followerCount?: Maybe; + id?: Maybe; + isOnline?: Maybe; + numSetups?: Maybe; + parentStreamId?: Maybe; + streamGame?: Maybe; + streamId?: Maybe; + streamLogo?: Maybe; + streamName?: Maybe; + streamSource?: Maybe; + streamStatus?: Maybe; + streamType?: Maybe; + streamTypeId?: Maybe; +}; + +/** A team, either at the global level or within the context of an event */ +export type Team = { + /** Uniquely identifying token for team. Same as the hashed part of the slug */ + discriminator?: Maybe; + /** @deprecated Use the entrant field off the EventTeam type */ + entrant?: Maybe; + /** @deprecated Use the event field off the EventTeam type */ + event?: Maybe; + id?: Maybe; + images?: Maybe>>; + members?: Maybe>>; + name?: Maybe; +}; + + +/** A team, either at the global level or within the context of an event */ +export type TeamImagesArgs = { + type?: InputMaybe; +}; + + +/** A team, either at the global level or within the context of an event */ +export type TeamMembersArgs = { + status?: InputMaybe>>; +}; + +/** A set of actions available for a team to take */ +export type TeamActionSet = ActionSet & { + __typename?: 'TeamActionSet'; + id?: Maybe; +}; + +export type TeamConnection = { + __typename?: 'TeamConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +/** A member of a team */ +export type TeamMember = { + __typename?: 'TeamMember'; + id?: Maybe; + isAlternate?: Maybe; + isCaptain?: Maybe; + /** The type of the team member */ + memberType?: Maybe; + participant?: Maybe; + player?: Maybe; + /** The status of the team member */ + status?: Maybe; +}; + +/** Membership status of a team member */ +export enum TeamMemberStatus { + Accepted = 'ACCEPTED', + Alum = 'ALUM', + Hiatus = 'HIATUS', + Invited = 'INVITED', + OpenSpot = 'OPEN_SPOT', + Request = 'REQUEST', + Unknown = 'UNKNOWN' +} + +/** Membership type of a team member */ +export enum TeamMemberType { + Player = 'PLAYER', + Staff = 'STAFF' +} + +export type TeamPaginationFilter = { + eventId?: InputMaybe; + eventIds?: InputMaybe>>; + eventState?: InputMaybe; + globalTeamId?: InputMaybe; + isLeague?: InputMaybe; + maxEntrantCount?: InputMaybe; + memberStatus?: InputMaybe>>; + minEntrantCount?: InputMaybe; + past?: InputMaybe; + rosterComplete?: InputMaybe; + rosterIncomplete?: InputMaybe; + search?: InputMaybe; + tournamentId?: InputMaybe; + type?: InputMaybe; + upcoming?: InputMaybe; + videogameId?: InputMaybe>>; +}; + +export type TeamPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** Team roster size requirements */ +export type TeamRosterSize = { + __typename?: 'TeamRosterSize'; + maxAlternates?: Maybe; + maxPlayers?: Maybe; + minAlternates?: Maybe; + minPlayers?: Maybe; +}; + +export type TopGameFilter = { + /** Array of which # top game you want to filter on.e.g. [2, 3] will filter on the 2nd and 3rd top games */ + gameNums?: InputMaybe>>; +}; + +/** A tournament */ +export type Tournament = { + __typename?: 'Tournament'; + addrState?: Maybe; + /** Admin-only view of admins for this tournament */ + admins?: Maybe>>; + city?: Maybe; + countryCode?: Maybe; + /** When the tournament was created (unix timestamp) */ + createdAt?: Maybe; + currency?: Maybe; + /** When the tournament ends */ + endAt?: Maybe; + /** When does event registration close */ + eventRegistrationClosesAt?: Maybe; + events?: Maybe>>; + /** True if tournament has at least one offline event */ + hasOfflineEvents?: Maybe; + hasOnlineEvents?: Maybe; + hashtag?: Maybe; + id?: Maybe; + images?: Maybe>>; + /** True if tournament has at least one online event */ + isOnline?: Maybe; + /** Is tournament registration open */ + isRegistrationOpen?: Maybe; + lat?: Maybe; + links?: Maybe; + lng?: Maybe; + mapsPlaceId?: Maybe; + /** The tournament name */ + name?: Maybe; + /** Number of attendees including spectators, if public */ + numAttendees?: Maybe; + /** The user who created the tournament */ + owner?: Maybe; + /** Paginated, queryable list of participants */ + participants?: Maybe; + postalCode?: Maybe; + primaryContact?: Maybe; + primaryContactType?: Maybe; + /** Publishing settings for this tournament */ + publishing?: Maybe; + /** When does registration for the tournament end */ + registrationClosesAt?: Maybe; + rules?: Maybe; + /** The short slug used to form the url */ + shortSlug?: Maybe; + /** The slug used to form the url */ + slug?: Maybe; + /** When the tournament Starts */ + startAt?: Maybe; + /** State of the tournament, can be ActivityState::CREATED, ActivityState::ACTIVE, or ActivityState::COMPLETED */ + state?: Maybe; + stations?: Maybe; + streamQueue?: Maybe>>; + streams?: Maybe>>; + /** When is the team creation deadline */ + teamCreationClosesAt?: Maybe; + /** Paginated, queryable list of teams */ + teams?: Maybe; + /** The timezone of the tournament */ + timezone?: Maybe; + /** The type of tournament from TournamentType */ + tournamentType?: Maybe; + /** When the tournament was last modified (unix timestamp) */ + updatedAt?: Maybe; + /** Build Tournament URL */ + url?: Maybe; + venueAddress?: Maybe; + venueName?: Maybe; + /** List of all waves in this tournament */ + waves?: Maybe>>; +}; + + +/** A tournament */ +export type TournamentAdminsArgs = { + roles?: InputMaybe>>; +}; + + +/** A tournament */ +export type TournamentEventsArgs = { + filter?: InputMaybe; + limit?: InputMaybe; +}; + + +/** A tournament */ +export type TournamentImagesArgs = { + type?: InputMaybe; +}; + + +/** A tournament */ +export type TournamentParticipantsArgs = { + isAdmin?: InputMaybe; + query: ParticipantPaginationQuery; +}; + + +/** A tournament */ +export type TournamentStationsArgs = { + page?: InputMaybe; + perPage?: InputMaybe; +}; + + +/** A tournament */ +export type TournamentTeamsArgs = { + query: TeamPaginationQuery; +}; + + +/** A tournament */ +export type TournamentUrlArgs = { + relative?: InputMaybe; + tab?: InputMaybe; +}; + +export type TournamentConnection = { + __typename?: 'TournamentConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type TournamentLinks = { + __typename?: 'TournamentLinks'; + discord?: Maybe; + facebook?: Maybe; +}; + +export type TournamentLocationFilter = { + /** e.g. 50mi */ + distance?: InputMaybe; + /** Latitude, Longitude */ + distanceFrom?: InputMaybe; +}; + +export type TournamentPageFilter = { + activeShops?: InputMaybe; + addrState?: InputMaybe; + afterDate?: InputMaybe; + beforeDate?: InputMaybe; + computedUpdatedAt?: InputMaybe; + countryCode?: InputMaybe; + hasBannerImages?: InputMaybe; + hasOnlineEvents?: InputMaybe; + id?: InputMaybe; + ids?: InputMaybe>>; + /** If true, filter to only tournaments the currently authed user is an admin of */ + isCurrentUserAdmin?: InputMaybe; + isFeatured?: InputMaybe; + isLeague?: InputMaybe; + location?: InputMaybe; + name?: InputMaybe; + /** ID of the user that owns this tournament. */ + ownerId?: InputMaybe; + past?: InputMaybe; + publiclySearchable?: InputMaybe; + published?: InputMaybe; + regOpen?: InputMaybe; + sortByScore?: InputMaybe; + staffPicks?: InputMaybe; + topGames?: InputMaybe; + upcoming?: InputMaybe; + venueName?: InputMaybe; + videogameIds?: InputMaybe>>; +}; + +export enum TournamentPaginationSort { + ComputedUpdatedAt = 'computedUpdatedAt', + EndAt = 'endAt', + EventRegistrationClosesAt = 'eventRegistrationClosesAt', + StartAt = 'startAt' +} + +export type TournamentQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sort?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type TournamentRegistrationInput = { + eventIds?: InputMaybe>>; +}; + +export type UpdatePhaseSeedInfo = { + phaseGroupId?: InputMaybe; + seedId: Scalars['ID']['input']; + seedNum: Scalars['ID']['input']; +}; + +export type UpdatePhaseSeedingOptions = { + /** Validate that seedMapping exactly accounts for all entrants in the phase */ + strictMode?: InputMaybe; +}; + +/** A user */ +export type User = { + __typename?: 'User'; + /** Authorizations to external services (i.e. Twitch, Twitter) */ + authorizations?: Maybe>>; + bio?: Maybe; + /** Public facing user birthday that respects user publishing settings */ + birthday?: Maybe; + /** Uniquely identifying token for user. Same as the hashed part of the slug */ + discriminator?: Maybe; + email?: Maybe; + /** Events this user has competed in */ + events?: Maybe; + genderPronoun?: Maybe; + id?: Maybe; + images?: Maybe>>; + /** Leagues this user has competed in */ + leagues?: Maybe; + /** Public location info for this user */ + location?: Maybe
; + /** Public facing user name that respects user publishing settings */ + name?: Maybe; + /** player for user */ + player?: Maybe; + slug?: Maybe; + /** Tournaments this user is organizing or competing in */ + tournaments?: Maybe; +}; + + +/** A user */ +export type UserAuthorizationsArgs = { + types?: InputMaybe>>; +}; + + +/** A user */ +export type UserEventsArgs = { + query?: InputMaybe; +}; + + +/** A user */ +export type UserImagesArgs = { + type?: InputMaybe; +}; + + +/** A user */ +export type UserLeaguesArgs = { + query?: InputMaybe; +}; + + +/** A user */ +export type UserTournamentsArgs = { + query?: InputMaybe; +}; + +export type UserEventsPaginationFilter = { + eventType?: InputMaybe; + location?: InputMaybe; + maxEntrantCount?: InputMaybe; + minEntrantCount?: InputMaybe; + search?: InputMaybe; + videogameId?: InputMaybe>>; +}; + +export type UserEventsPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type UserLeaguesPaginationFilter = { + past?: InputMaybe; + search?: InputMaybe; + upcoming?: InputMaybe; + videogameId?: InputMaybe>>; +}; + +export type UserLeaguesPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +export type UserTournamentsPaginationFilter = { + excludeId?: InputMaybe>>; + past?: InputMaybe; + search?: InputMaybe; + tournamentView?: InputMaybe; + upcoming?: InputMaybe; + videogameId?: InputMaybe>>; +}; + +export type UserTournamentsPaginationQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** A videogame */ +export type Videogame = { + __typename?: 'Videogame'; + /** All characters for this videogame */ + characters?: Maybe>>; + displayName?: Maybe; + id?: Maybe; + images?: Maybe>>; + name?: Maybe; + slug?: Maybe; + /** All stages for this videogame */ + stages?: Maybe>>; +}; + + +/** A videogame */ +export type VideogameImagesArgs = { + type?: InputMaybe; +}; + +export type VideogameConnection = { + __typename?: 'VideogameConnection'; + nodes?: Maybe>>; + pageInfo?: Maybe; +}; + +export type VideogamePageFilter = { + forUser?: InputMaybe; + id?: InputMaybe>>; + name?: InputMaybe; +}; + +export type VideogameQuery = { + filter?: InputMaybe; + page?: InputMaybe; + /** How many nodes to return for the page. Maximum value of 500 */ + perPage?: InputMaybe; + sortBy?: InputMaybe; +}; + +/** A wave in a tournament */ +export type Wave = { + __typename?: 'Wave'; + id?: Maybe; + /** The Wave Identifier */ + identifier?: Maybe; + /** Unix time the wave is scheduled to start. */ + startAt?: Maybe; +}; + +export type WaveUpsertInput = { + endAt: Scalars['Timestamp']['input']; + identifier: Scalars['String']['input']; + startAt: Scalars['Timestamp']['input']; +}; + +export type EventEntrantsQueryVariables = Exact<{ + eventSlug?: InputMaybe; +}>; + + +export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, name?: string | null } | null }; + + +export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/startgg-gql/generated/index.ts b/src/startgg-gql/generated/index.ts new file mode 100644 index 000000000..f51599168 --- /dev/null +++ b/src/startgg-gql/generated/index.ts @@ -0,0 +1,2 @@ +export * from "./fragment-masking"; +export * from "./gql"; \ No newline at end of file diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts new file mode 100644 index 000000000..deb490bf5 --- /dev/null +++ b/src/startgg-gql/index.ts @@ -0,0 +1,16 @@ +import { EventEntrantsDocument } from "./generated/graphql"; +import { Client, cacheExchange, fetchExchange } from "@urql/core"; + +const client = new Client({ + url: "https://api.start.gg/gql/alpha", + fetchOptions: { + headers: { + Authorization: `Bearer ${process.env.STARTGG_TOKEN}`, + }, + }, + exchanges: [cacheExchange, fetchExchange], +}); + +export function getEventEntrants(slug: string) { + return client.query(EventEntrantsDocument, { eventSlug: slug }); +} diff --git a/src/startgg-gql/queries.ts b/src/startgg-gql/queries.ts new file mode 100644 index 000000000..9f6e66085 --- /dev/null +++ b/src/startgg-gql/queries.ts @@ -0,0 +1,10 @@ +import { gql } from "@urql/core"; + +gql` + query EventEntrants($eventSlug: String) { + event(slug: $eventSlug) { + id + name + } + } +`; diff --git a/webpack.config.js b/webpack.config.js index a7f7ac13a..0c43b5464 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,4 @@ +require("dotenv").setup(); const fs = require("fs"); const { resolve, basename } = require("path"); @@ -191,6 +192,7 @@ module.exports = function (env = {}, argv = {}) { }; }), ), + "process.env.STARTGG_TOKEN": JSON.stringify(process.env.STARTGG_TOKEN), }), new MiniCssExtractPlugin({ filename: "[name].[chunkhash:5].css", diff --git a/yarn.lock b/yarn.lock index e5c19943e..2fae8661c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,18 @@ __metadata: version: 8 cacheKey: 10 +"@0no-co/graphql.web@npm:^1.0.5": + version: 1.0.7 + resolution: "@0no-co/graphql.web@npm:1.0.7" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + checksum: 10/8be5d80ba57df759a11f905ba7ac4bc6e9bc75034804bb679db4fa4eed3396f97b0191421031ba2d94af259fa7d55042a152e3f94294eba62ae81591d41bad87 + languageName: node + linkType: hard + "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" @@ -33,6 +45,44 @@ __metadata: languageName: node linkType: hard +"@ardatan/relay-compiler@npm:12.0.0": + version: 12.0.0 + resolution: "@ardatan/relay-compiler@npm:12.0.0" + dependencies: + "@babel/core": "npm:^7.14.0" + "@babel/generator": "npm:^7.14.0" + "@babel/parser": "npm:^7.14.0" + "@babel/runtime": "npm:^7.0.0" + "@babel/traverse": "npm:^7.14.0" + "@babel/types": "npm:^7.0.0" + babel-preset-fbjs: "npm:^3.4.0" + chalk: "npm:^4.0.0" + fb-watchman: "npm:^2.0.0" + fbjs: "npm:^3.0.0" + glob: "npm:^7.1.1" + immutable: "npm:~3.7.6" + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + relay-runtime: "npm:12.0.0" + signedsource: "npm:^1.0.0" + yargs: "npm:^15.3.1" + peerDependencies: + graphql: "*" + bin: + relay-compiler: bin/relay-compiler + checksum: 10/60896560fd282ccc9e705fa18c685d23783f97670fa44be287beaf9d49acfd1a6bbc19daf3e55d9cffdf385ef883be36f7acf5bdcf61c46483e31db9e4e71884 + languageName: node + linkType: hard + +"@ardatan/sync-fetch@npm:^0.0.1": + version: 0.0.1 + resolution: "@ardatan/sync-fetch@npm:0.0.1" + dependencies: + node-fetch: "npm:^2.6.1" + checksum: 10/ee21741badecb18fb9a18a404275e25272f67ade914f98885de79ccecba3403b8a6357e6b033a028e24f0d902197dd541655309d7789ebacd7ad981bf1f12618 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.7": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" @@ -43,6 +93,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/compat-data@npm:7.24.8" + checksum: 10/6989b8a61782d9c6c7a1fc58b4efd4fb68e5f5a5b6be3463a3de3752f39a30d21438b8b4485c18cb6b8d7f29e07f79d79639caa08737fae57838e81d7da055c0 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.24.7": version: 7.24.7 resolution: "@babel/compat-data@npm:7.24.7" @@ -50,6 +107,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.14.0, @babel/core@npm:^7.22.9": + version: 7.24.8 + resolution: "@babel/core@npm:7.24.8" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-compilation-targets": "npm:^7.24.8" + "@babel/helper-module-transforms": "npm:^7.24.8" + "@babel/helpers": "npm:^7.24.8" + "@babel/parser": "npm:^7.24.8" + "@babel/template": "npm:^7.24.7" + "@babel/traverse": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/79818e6e8ecd5f50ffbfb8dfb1748928e6e17b198bd8da0d6f725bf67aece5141020cd3aba56dc425a33e118c0e80e5569ad6fa615897e49726087dd875279d7 + languageName: node + linkType: hard + "@babel/core@npm:^7.24": version: 7.24.7 resolution: "@babel/core@npm:7.24.7" @@ -73,6 +153,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/generator@npm:7.24.8" + dependencies: + "@babel/types": "npm:^7.24.8" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/dc1bd931120f93e7a5b35fdf66c13ca56b966b07ee9ba124f7e24b1905cbcf7d7891cc7c281961876eff9fcff67c46652cce89847665e263bc04d283d4343164 + languageName: node + linkType: hard + "@babel/generator@npm:^7.24.7": version: 7.24.7 resolution: "@babel/generator@npm:7.24.7" @@ -104,6 +196,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-compilation-targets@npm:7.24.8" + dependencies: + "@babel/compat-data": "npm:^7.24.8" + "@babel/helper-validator-option": "npm:^7.24.8" + browserslist: "npm:^4.23.1" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/3489280d07b871af565b32f9b11946ff9a999fac0db9bec5df960760f6836c7a4b52fccb9d64229ccce835d37a43afb85659beb439ecedde04dcea7eb062a143 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-compilation-targets@npm:7.24.7" @@ -117,6 +222,25 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.18.6": + version: 7.24.8 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.8" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-member-expression-to-functions": "npm:^7.24.8" + "@babel/helper-optimise-call-expression": "npm:^7.24.7" + "@babel/helper-replace-supers": "npm:^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/a779c5356fcc4881e807d85d973fd37e99e773fe95837b0f6582ca9a89331f84e5f26b0b6aa9a101181325b73cf3f54081d178b657a79819b8abadc53b0ea8ec + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.7" @@ -202,6 +326,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-member-expression-to-functions@npm:7.24.8" + dependencies: + "@babel/traverse": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + checksum: 10/ac878761cfd0a46c081cda0da75cc186f922cf16e8ecdd0c4fb6dca4330d9fe4871b41a9976224cf9669c9e7fe0421b5c27349f2e99c125fa0be871b327fa770 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-module-imports@npm:7.24.7" @@ -227,6 +361,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-module-transforms@npm:7.24.8" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/912ad994da126c3150d8f8702030380849608094a7a352523ffa8e697080da9358d63af2582d38902c929839f394bbc6f1ae4921ba132ba3f65f27f0696aa2c7 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" @@ -243,6 +392,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-plugin-utils@npm:7.24.8" + checksum: 10/adbc9fc1142800a35a5eb0793296924ee8057fe35c61657774208670468a9fbfbb216f2d0bc46c680c5fefa785e5ff917cc1674b10bd75cdf9a6aa3444780630 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-remap-async-to-generator@npm:7.24.7" @@ -305,6 +461,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10/6d1bf8f27dd725ce02bdc6dffca3c95fb9ab8a06adc2edbd9c1c9d68500274230d1a609025833ed81981eff560045b6b38f7b4c6fb1ab19fc90e5004e3932535 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-identifier@npm:7.24.7" @@ -319,6 +482,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: 10/a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-wrap-function@npm:7.24.7" @@ -341,6 +511,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helpers@npm:7.24.8" + dependencies: + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.8" + checksum: 10/61c08a2baa87382a87c7110e9b5574c782603e247b7e6267769ee0e8b7b54b70ff05f16466f05bb318622b7ac28e79b449edff565abf5adcb1adb1b0f42fee9c + languageName: node + linkType: hard + "@babel/highlight@npm:^7.24.7": version: 7.24.7 resolution: "@babel/highlight@npm:7.24.7" @@ -353,6 +533,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.14.0, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/parser@npm:7.24.8" + bin: + parser: ./bin/babel-parser.js + checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + languageName: node + linkType: hard + "@babel/parser@npm:^7.24.7": version: 7.24.7 resolution: "@babel/parser@npm:7.24.7" @@ -410,6 +599,33 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-class-properties@npm:^7.0.0": + version: 7.18.6 + resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.18.6" + "@babel/helper-plugin-utils": "npm:^7.18.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/49a78a2773ec0db56e915d9797e44fd079ab8a9b2e1716e0df07c92532f2c65d76aeda9543883916b8e0ff13606afeffa67c5b93d05b607bc87653ad18a91422 + languageName: node + linkType: hard + +"@babel/plugin-proposal-object-rest-spread@npm:^7.0.0": + version: 7.20.7 + resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.7" + dependencies: + "@babel/compat-data": "npm:^7.20.5" + "@babel/helper-compilation-targets": "npm:^7.20.7" + "@babel/helper-plugin-utils": "npm:^7.20.2" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-transform-parameters": "npm:^7.20.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/cb0f8f2ff98d7bb64ee91c28b20e8ab15d9bc7043f0932cbb9e51e1bbfb623b12f206a1171e070299c9cf21948c320b710d6d72a42f68a5bfd2702354113a1c5 + languageName: node + linkType: hard + "@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2": version: 7.21.0-placeholder-for-preset-env.2 resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2" @@ -430,7 +646,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-class-properties@npm:^7.12.13": +"@babel/plugin-syntax-class-properties@npm:^7.0.0, @babel/plugin-syntax-class-properties@npm:^7.12.13": version: 7.12.13 resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" dependencies: @@ -474,7 +690,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.24.7": +"@babel/plugin-syntax-flow@npm:^7.0.0, @babel/plugin-syntax-flow@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/plugin-syntax-flow@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0a83bde6736110d68f3b20eda44ca020a6d34c336a342f84369207f5514e17779b9c3d3ebc2f1c94b595c13819f46bf7af367c4b1382bda182e1764655fd6a5a + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-assertions@npm:^7.20.0, @babel/plugin-syntax-import-assertions@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.7" dependencies: @@ -518,7 +745,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.24.7": +"@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-syntax-jsx@npm:7.24.7" dependencies: @@ -562,7 +789,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": +"@babel/plugin-syntax-object-rest-spread@npm:^7.0.0, @babel/plugin-syntax-object-rest-spread@npm:^7.8.3": version: 7.8.3 resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" dependencies: @@ -640,7 +867,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.24.7": +"@babel/plugin-transform-arrow-functions@npm:^7.0.0, @babel/plugin-transform-arrow-functions@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.7" dependencies: @@ -678,7 +905,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.24.7": +"@babel/plugin-transform-block-scoped-functions@npm:^7.0.0, @babel/plugin-transform-block-scoped-functions@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.7" dependencies: @@ -689,7 +916,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.24.7": +"@babel/plugin-transform-block-scoping@npm:^7.0.0, @babel/plugin-transform-block-scoping@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-block-scoping@npm:7.24.7" dependencies: @@ -725,6 +952,24 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.0.0": + version: 7.24.8 + resolution: "@babel/plugin-transform-classes@npm:7.24.8" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.24.7" + "@babel/helper-compilation-targets": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-replace-supers": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + globals: "npm:^11.1.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3d586018691423ed1fbcb4589cc29001226c96e5e060932bf99379568c684a4a230cca7871e7c825335336ef0326066ba6e3bf5e6d0209425b0f5ceeda3eaed2 + languageName: node + linkType: hard + "@babel/plugin-transform-classes@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-classes@npm:7.24.7" @@ -743,7 +988,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.24.7": +"@babel/plugin-transform-computed-properties@npm:^7.0.0, @babel/plugin-transform-computed-properties@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-computed-properties@npm:7.24.7" dependencies: @@ -755,6 +1000,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.0.0": + version: 7.24.8 + resolution: "@babel/plugin-transform-destructuring@npm:7.24.8" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.8" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e3bba0bb050592615fbf062ea07ae94f99e9cf22add006eaa66ed672d67ff7051b578a5ea68a7d79f9184fb3c27c65333d86b0b8ea04f9810bcccbeea2ffbe76 + languageName: node + linkType: hard + "@babel/plugin-transform-destructuring@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-destructuring@npm:7.24.7" @@ -825,7 +1081,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.24.7": +"@babel/plugin-transform-flow-strip-types@npm:^7.0.0": + version: 7.24.7 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/plugin-syntax-flow": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/234390eb09f0c1d5a2001c9e48c6440c30f9f188939004e07aa8c0cb946f04793a2e058fa1737b1c56041a7d3ea1510593c39220cc43bba85a017bfcc1c89c4d + languageName: node + linkType: hard + +"@babel/plugin-transform-for-of@npm:^7.0.0, @babel/plugin-transform-for-of@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-for-of@npm:7.24.7" dependencies: @@ -837,7 +1105,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.24.7": +"@babel/plugin-transform-function-name@npm:^7.0.0, @babel/plugin-transform-function-name@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-function-name@npm:7.24.7" dependencies: @@ -862,7 +1130,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.24.7": +"@babel/plugin-transform-literals@npm:^7.0.0, @babel/plugin-transform-literals@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-literals@npm:7.24.7" dependencies: @@ -885,7 +1153,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.24.7": +"@babel/plugin-transform-member-expression-literals@npm:^7.0.0, @babel/plugin-transform-member-expression-literals@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.7" dependencies: @@ -908,6 +1176,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.0.0": + version: 7.24.8 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.8" + dependencies: + "@babel/helper-module-transforms": "npm:^7.24.8" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/helper-simple-access": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/18e5d229767c7b5b6ff0cbf1a8d2d555965b90201839d0ac2dc043b56857624ea344e59f733f028142a8c1d54923b82e2a0185694ef36f988d797bfbaf59819c + languageName: node + linkType: hard + "@babel/plugin-transform-modules-commonjs@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.7" @@ -1008,7 +1289,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.24.7": +"@babel/plugin-transform-object-super@npm:^7.0.0, @babel/plugin-transform-object-super@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-object-super@npm:7.24.7" dependencies: @@ -1045,7 +1326,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.24.7": +"@babel/plugin-transform-parameters@npm:^7.0.0, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-parameters@npm:7.24.7" dependencies: @@ -1082,7 +1363,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.24.7": +"@babel/plugin-transform-property-literals@npm:^7.0.0, @babel/plugin-transform-property-literals@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-property-literals@npm:7.24.7" dependencies: @@ -1093,6 +1374,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.0.0": + version: 7.24.7 + resolution: "@babel/plugin-transform-react-display-name@npm:7.24.7" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.7" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f5d34903680ca358c5a3ccb83421df259e5142be95dde51dc4a62ec79fd6558599b3b92b4afd37329d2567a4ba4c338f1c817f8ce0c56ddf20cd3d051498649e + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-source@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-react-jsx-source@npm:7.24.7" @@ -1104,7 +1396,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.24": +"@babel/plugin-transform-react-jsx@npm:^7.0.0, @babel/plugin-transform-react-jsx@npm:^7.24": version: 7.24.7 resolution: "@babel/plugin-transform-react-jsx@npm:7.24.7" dependencies: @@ -1142,7 +1434,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.24.7": +"@babel/plugin-transform-shorthand-properties@npm:^7.0.0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.7" dependencies: @@ -1153,7 +1445,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.24.7": +"@babel/plugin-transform-spread@npm:^7.0.0, @babel/plugin-transform-spread@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-spread@npm:7.24.7" dependencies: @@ -1176,7 +1468,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.24.7": +"@babel/plugin-transform-template-literals@npm:^7.0.0, @babel/plugin-transform-template-literals@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-template-literals@npm:7.24.7" dependencies: @@ -1385,6 +1677,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.0.0": + version: 7.24.8 + resolution: "@babel/runtime@npm:7.24.8" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/e6f335e472a8a337379effc15815dd0eddf6a7d0c00b50deb4f9e9585819b45431d0ff3c2d3d0fa58c227a9b04dcc4a85e7245fb57493adb2863b5208c769cbd + languageName: node + linkType: hard + "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.24.4 resolution: "@babel/runtime@npm:7.24.4" @@ -1394,7 +1695,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.24.7": +"@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.24.7": version: 7.24.7 resolution: "@babel/template@npm:7.24.7" dependencies: @@ -1405,6 +1706,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/traverse@npm:7.24.8" + dependencies: + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-hoist-variables": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.24.7": version: 7.24.7 resolution: "@babel/traverse@npm:7.24.7" @@ -1423,6 +1742,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/types@npm:7.24.8" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/29b080b2753c22ee5e2455ff767a971443245d945dea4d1b3130e036dcdf0949a89539a581753c68d03d2f2f2325244ee0f91fb83dabee1cbac5db5246838137 + languageName: node + linkType: hard + "@babel/types@npm:^7.24.7, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.24.7 resolution: "@babel/types@npm:7.24.7" @@ -1787,139 +2117,728 @@ __metadata: version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" dependencies: - eslint-visitor-keys: "npm:^3.3.0" + eslint-visitor-keys: "npm:^3.3.0" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10/8d70bcdcd8cd279049183aca747d6c2ed7092a5cf0cf5916faac1ef37ffa74f0c245c2a3a3d3b9979d9dfdd4ca59257b4c5621db699d637b847a2c5e02f491c2 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1": + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 10/8c36169c815fc5d726078e8c71a5b592957ee60d08c6470f9ce0187c8046af1a00afbda0a065cc40ff18d5d83f82aed9793c6818f7304a74a7488dc9f3ecbd42 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 + languageName: node + linkType: hard + +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 + languageName: node + linkType: hard + +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 10/2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 + languageName: node + linkType: hard + +"@formatjs/ecma402-abstract@npm:2.0.0": + version: 2.0.0 + resolution: "@formatjs/ecma402-abstract@npm:2.0.0" + dependencies: + "@formatjs/intl-localematcher": "npm:0.5.4" + tslib: "npm:^2.4.0" + checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234 + languageName: node + linkType: hard + +"@formatjs/fast-memoize@npm:2.2.0": + version: 2.2.0 + resolution: "@formatjs/fast-memoize@npm:2.2.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/8697fe72a7ece252d600a7d08105f2a2f758e2dd96f54ac0a4c508b1205a559fc08835635e1f8e5ca9dcc3ee61ce1fca4a0e7047b402f29fc96051e293a280ff + languageName: node + linkType: hard + +"@formatjs/icu-messageformat-parser@npm:2.7.8": + version: 2.7.8 + resolution: "@formatjs/icu-messageformat-parser@npm:2.7.8" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/icu-skeleton-parser": "npm:1.8.2" + tslib: "npm:^2.4.0" + checksum: 10/292fd36268ad84337c0e798fc73b58e8f3cf3f362cea031f710fd78053d29b420526ab766a95745e162ae4a11bf846bc2f7ae5c2c0a3288d3bc9daa97a3be8c1 + languageName: node + linkType: hard + +"@formatjs/icu-skeleton-parser@npm:1.8.2": + version: 1.8.2 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.2" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + tslib: "npm:^2.4.0" + checksum: 10/a06b61cf6c298bbbc23349e391bad8a1cf0a6a32dc4928a4681a3aa6f38dd8c6a181dc4067e228f67584d4dc181d862704095e65c38cfac077c984dc24ba54d3 + languageName: node + linkType: hard + +"@formatjs/intl-displaynames@npm:6.6.8": + version: 6.6.8 + resolution: "@formatjs/intl-displaynames@npm:6.6.8" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/intl-localematcher": "npm:0.5.4" + tslib: "npm:^2.4.0" + checksum: 10/d305787fe2a35f9725b1293f70a2ccc454310c5df48ad62613d674a648dd318bec2a3ccd8656047443d1e438bb1cf8140e354d9c4a4101ec38df00614e0244b7 + languageName: node + linkType: hard + +"@formatjs/intl-listformat@npm:7.5.7": + version: 7.5.7 + resolution: "@formatjs/intl-listformat@npm:7.5.7" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/intl-localematcher": "npm:0.5.4" + tslib: "npm:^2.4.0" + checksum: 10/ba7e1ee9c29f2a8cfc66d51c5f273f279ced048b427533aa3d502cf7d12d510e79965d0471a158fd41d7cc7314f688c56d863c3ebec8e5e550d687f64d210794 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.5.4": + version: 0.5.4 + resolution: "@formatjs/intl-localematcher@npm:0.5.4" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778 + languageName: node + linkType: hard + +"@formatjs/intl@npm:2.10.4": + version: 2.10.4 + resolution: "@formatjs/intl@npm:2.10.4" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/fast-memoize": "npm:2.2.0" + "@formatjs/icu-messageformat-parser": "npm:2.7.8" + "@formatjs/intl-displaynames": "npm:6.6.8" + "@formatjs/intl-listformat": "npm:7.5.7" + intl-messageformat: "npm:10.5.14" + tslib: "npm:^2.4.0" + peerDependencies: + typescript: ^4.7 || 5 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/1d4e60cfb3edecb51a5f8dd2c2b1c784c1a9bfcbc1b6240323d8009e5f375e5c526c9eb19149b42e0e50f6a473e9709378071658ed25a62dc3923dd7fc6a2843 + languageName: node + linkType: hard + +"@graphql-codegen/add@npm:^5.0.3": + version: 5.0.3 + resolution: "@graphql-codegen/add@npm:5.0.3" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.3" + tslib: "npm:~2.6.0" + 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 + checksum: 10/5e4ea9b5d76e6f472015185d0d007abf38ba7b27fb613b163ef1ca6c732695951111721896ee36894d098b27a67c55a84d000b503db93b9d9e6713e34d2fa5a6 + languageName: node + linkType: hard + +"@graphql-codegen/cli@npm:^5.0.2": + version: 5.0.2 + resolution: "@graphql-codegen/cli@npm:5.0.2" + dependencies: + "@babel/generator": "npm:^7.18.13" + "@babel/template": "npm:^7.18.10" + "@babel/types": "npm:^7.18.13" + "@graphql-codegen/client-preset": "npm:^4.2.2" + "@graphql-codegen/core": "npm:^4.0.2" + "@graphql-codegen/plugin-helpers": "npm:^5.0.3" + "@graphql-tools/apollo-engine-loader": "npm:^8.0.0" + "@graphql-tools/code-file-loader": "npm:^8.0.0" + "@graphql-tools/git-loader": "npm:^8.0.0" + "@graphql-tools/github-loader": "npm:^8.0.0" + "@graphql-tools/graphql-file-loader": "npm:^8.0.0" + "@graphql-tools/json-file-loader": "npm:^8.0.0" + "@graphql-tools/load": "npm:^8.0.0" + "@graphql-tools/prisma-loader": "npm:^8.0.0" + "@graphql-tools/url-loader": "npm:^8.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + "@whatwg-node/fetch": "npm:^0.8.0" + chalk: "npm:^4.1.0" + cosmiconfig: "npm:^8.1.3" + debounce: "npm:^1.2.0" + detect-indent: "npm:^6.0.0" + graphql-config: "npm:^5.0.2" + inquirer: "npm:^8.0.0" + is-glob: "npm:^4.0.1" + jiti: "npm:^1.17.1" + json-to-pretty-yaml: "npm:^1.2.2" + listr2: "npm:^4.0.5" + log-symbols: "npm:^4.0.0" + micromatch: "npm:^4.0.5" + shell-quote: "npm:^1.7.3" + string-env-interpolation: "npm:^1.0.1" + ts-log: "npm:^2.2.3" + tslib: "npm:^2.4.0" + yaml: "npm:^2.3.1" + yargs: "npm:^17.0.0" + peerDependencies: + "@parcel/watcher": ^2.1.0 + 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 + peerDependenciesMeta: + "@parcel/watcher": + optional: true + bin: + gql-gen: cjs/bin.js + graphql-code-generator: cjs/bin.js + graphql-codegen: cjs/bin.js + graphql-codegen-esm: esm/bin.js + checksum: 10/24f5a4d441e4af2f0cae1818c8643a5400718cc1f08ca829a9110a35d99cb5529b567991ce826544b5a2aab36d0be3b10309dc112343bab1232d7c6f2fa14008 + languageName: node + linkType: hard + +"@graphql-codegen/client-preset@npm:^4.2.2, @graphql-codegen/client-preset@npm:^4.3.2": + version: 4.3.2 + resolution: "@graphql-codegen/client-preset@npm:4.3.2" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.20.2" + "@babel/template": "npm:^7.20.7" + "@graphql-codegen/add": "npm:^5.0.3" + "@graphql-codegen/gql-tag-operations": "npm:4.0.9" + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/typed-document-node": "npm:^5.0.9" + "@graphql-codegen/typescript": "npm:^4.0.9" + "@graphql-codegen/typescript-operations": "npm:^4.2.3" + "@graphql-codegen/visitor-plugin-common": "npm:^5.3.1" + "@graphql-tools/documents": "npm:^1.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-typed-document-node/core": "npm:3.2.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/52c66f1f0de4a1537726d2d6915f307aa650ab425a9b6ba5e24718a412f5b2aa7b27c706074d7fa086cbede7c0a0fb74ff1016ba378cc729a40e301d207a42f8 + languageName: node + linkType: hard + +"@graphql-codegen/core@npm:^4.0.2": + version: 4.0.2 + resolution: "@graphql-codegen/core@npm:4.0.2" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.3" + "@graphql-tools/schema": "npm:^10.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/09aa9d5b3215b7c8a81e07d6c826fa9697e4d20c7fa4333905aa89afe88044ce5c733633a59c6590fc997f03a6f62f9aecf76d6c1efa4f1a16c5ad2b0b6f665b + languageName: node + linkType: hard + +"@graphql-codegen/gql-tag-operations@npm:4.0.9": + version: 4.0.9 + resolution: "@graphql-codegen/gql-tag-operations@npm:4.0.9" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + "@graphql-tools/utils": "npm:^10.0.0" + auto-bind: "npm:~4.0.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/90eaecbe0742ee65f0e25df471760664d615bb6396d7e35c2d33615128849c1bdb1c0479c75de589479b7b1a0ee99b34684a2faeeff6245e01ffa7dcfff4b0c6 + languageName: node + linkType: hard + +"@graphql-codegen/plugin-helpers@npm:^5.0.3, @graphql-codegen/plugin-helpers@npm:^5.0.4": + version: 5.0.4 + resolution: "@graphql-codegen/plugin-helpers@npm:5.0.4" + dependencies: + "@graphql-tools/utils": "npm:^10.0.0" + change-case-all: "npm:1.0.15" + common-tags: "npm:1.8.2" + import-from: "npm:4.0.0" + lodash: "npm:~4.17.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/8162bffc76bf0d6cd9ff83c98b8a5e5eadbb1bc0de2d273480af937a27ca8fbf74aae72a617303a9d4121b9914eb9af065858f07c0ac13cd169b53a9bcead799 + languageName: node + linkType: hard + +"@graphql-codegen/schema-ast@npm:^4.0.2": + version: 4.1.0 + resolution: "@graphql-codegen/schema-ast@npm:4.1.0" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.3" + "@graphql-tools/utils": "npm:^10.0.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/cddec7723d708990ac8e33eb8935e72545b60ed7b772452ba45b60e577af950d23503de83f0919d1730f7d52dcb970900d3587d9a54202032164ba3c246d4c10 + languageName: node + linkType: hard + +"@graphql-codegen/typed-document-node@npm:^5.0.9": + version: 5.0.9 + resolution: "@graphql-codegen/typed-document-node@npm:5.0.9" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + auto-bind: "npm:~4.0.0" + change-case-all: "npm:1.0.15" + tslib: "npm:~2.6.0" + 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 + checksum: 10/0cac39bf2d8304660dc99b46b9c6ed811be79aca8a2cd1f58d9a16ff83fb644e985d872456eed5fdba27fa1727e76c3d3190e251c0171d83aff1af58cbcbc3c8 + languageName: node + linkType: hard + +"@graphql-codegen/typescript-operations@npm:^4.2.3": + version: 4.2.3 + resolution: "@graphql-codegen/typescript-operations@npm:4.2.3" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/typescript": "npm:^4.0.9" + "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + auto-bind: "npm:~4.0.0" + tslib: "npm:~2.6.0" + 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 + checksum: 10/b501b43c5a686847e12812690e0aea3842c6efa10d53703db063d82f2a4f3c469bb02c430a29ac1d1b450ffc82cf97c0ca1b9caa8e8979279f72646f010301bf + languageName: node + linkType: hard + +"@graphql-codegen/typescript@npm:^4.0.9": + version: 4.0.9 + resolution: "@graphql-codegen/typescript@npm:4.0.9" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-codegen/schema-ast": "npm:^4.0.2" + "@graphql-codegen/visitor-plugin-common": "npm:5.3.1" + auto-bind: "npm:~4.0.0" + tslib: "npm:~2.6.0" + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/304026adfe622530b8a2827569dd5bbd390177051be8c214fb79873ec64ef21793635c91657703bfd229a3d06f1a8a6f1addd8ae7eab20d1eff2efe6fb044df7 + languageName: node + linkType: hard + +"@graphql-codegen/visitor-plugin-common@npm:5.3.1, @graphql-codegen/visitor-plugin-common@npm:^5.3.1": + version: 5.3.1 + resolution: "@graphql-codegen/visitor-plugin-common@npm:5.3.1" + dependencies: + "@graphql-codegen/plugin-helpers": "npm:^5.0.4" + "@graphql-tools/optimize": "npm:^2.0.0" + "@graphql-tools/relay-operation-optimizer": "npm:^7.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + auto-bind: "npm:~4.0.0" + change-case-all: "npm:1.0.15" + dependency-graph: "npm:^0.11.0" + graphql-tag: "npm:^2.11.0" + parse-filepath: "npm:^1.0.2" + tslib: "npm:~2.6.0" + 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 + checksum: 10/6dd0464d9099d5aeabeb766515fc8dd2fc84bcae4cb0e3653d7f38aea716d6622d35d7cbb57a1954e6bc1cde10f4dd8c4a75ceb4e8bb8cdbba16219615666a5f + languageName: node + linkType: hard + +"@graphql-tools/apollo-engine-loader@npm:^8.0.0": + version: 8.0.1 + resolution: "@graphql-tools/apollo-engine-loader@npm:8.0.1" + dependencies: + "@ardatan/sync-fetch": "npm:^0.0.1" + "@graphql-tools/utils": "npm:^10.0.13" + "@whatwg-node/fetch": "npm:^0.9.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/4baef5a8fd85787568188f852446e4f2453a21026c60b9391641093bff0a88172df8beb8bbfe2b134e177acad90eeb613387a1185699d1e7718e1dfa701c6fd7 + languageName: node + linkType: hard + +"@graphql-tools/batch-execute@npm:^9.0.4": + version: 9.0.4 + resolution: "@graphql-tools/batch-execute@npm:9.0.4" + dependencies: + "@graphql-tools/utils": "npm:^10.0.13" + dataloader: "npm:^2.2.2" + tslib: "npm:^2.4.0" + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/955647a094f787138bccd6cf33e06cf5bb5bf451cfad66a613f8c54ffd5a1f1b48342136e4e3d32fcf559ee1e2c438c98f5fd42cf99b19b3e5017449bea8a707 + languageName: node + linkType: hard + +"@graphql-tools/code-file-loader@npm:^8.0.0": + version: 8.1.2 + resolution: "@graphql-tools/code-file-loader@npm:8.1.2" + dependencies: + "@graphql-tools/graphql-tag-pluck": "npm:8.3.1" + "@graphql-tools/utils": "npm:^10.0.13" + globby: "npm:^11.0.3" + tslib: "npm:^2.4.0" + unixify: "npm:^1.0.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/691a58c8c7584127a5c1c7a162b3cfde0597f422adfa757bbb790c7ccda5116c7ca606860e929958c2368416adce7b23ac4234a8f8df59c2a28736540b31feab + languageName: node + linkType: hard + +"@graphql-tools/delegate@npm:^10.0.4": + version: 10.0.13 + resolution: "@graphql-tools/delegate@npm:10.0.13" + dependencies: + "@graphql-tools/batch-execute": "npm:^9.0.4" + "@graphql-tools/executor": "npm:^1.2.8" + "@graphql-tools/schema": "npm:^10.0.4" + "@graphql-tools/utils": "npm:^10.2.3" + dataloader: "npm:^2.2.2" + tslib: "npm:^2.5.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/fb200442d12271dc4368a9b5ff30f8f801f6b42af68fa31ff1c6f77514a9d459f24ed8fc387ff943bc4864e623751d2406692c1cadf5df877f5bd5e9c41ac359 + languageName: node + linkType: hard + +"@graphql-tools/documents@npm:^1.0.0": + version: 1.0.1 + resolution: "@graphql-tools/documents@npm:1.0.1" + dependencies: + lodash.sortby: "npm:^4.7.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/6af5cc1a5ab88fc2ef08d97c1190c4857ea894ea41672f9f94889ed817664524972c8f234bed023b0746fd2f358b96ca1cc753f0af127d0b8076fa7c6f3e27e5 + languageName: node + linkType: hard + +"@graphql-tools/executor-graphql-ws@npm:^1.1.2": + version: 1.2.0 + resolution: "@graphql-tools/executor-graphql-ws@npm:1.2.0" + dependencies: + "@graphql-tools/utils": "npm:^10.3.0" + "@types/ws": "npm:^8.0.0" + graphql-ws: "npm:^5.14.0" + isomorphic-ws: "npm:^5.0.0" + tslib: "npm:^2.4.0" + ws: "npm:^8.17.1" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/501824d3608c17109ab3505639215ed46b416a53329352b60ef63e39611be2e33d19f3ad882eb427ca27c9c65330d94a477cd1fd45f1098957b51d221d0a57b2 + languageName: node + linkType: hard + +"@graphql-tools/executor-http@npm:^1.0.9": + version: 1.1.4 + resolution: "@graphql-tools/executor-http@npm:1.1.4" + dependencies: + "@graphql-tools/utils": "npm:^10.3.2" + "@repeaterjs/repeater": "npm:^3.0.4" + "@whatwg-node/fetch": "npm:^0.9.0" + extract-files: "npm:^11.0.0" + meros: "npm:^1.2.1" + tslib: "npm:^2.4.0" + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/6a1296cde5acf4e021bf5ffbb8123b6deb1995198657b662eb9fef3d3c8f0c04a1d90f934c864f5877002906daa5af2da895960b51269d1c905ddafee5411e0c + languageName: node + linkType: hard + +"@graphql-tools/executor-legacy-ws@npm:^1.0.6": + version: 1.1.0 + resolution: "@graphql-tools/executor-legacy-ws@npm:1.1.0" + dependencies: + "@graphql-tools/utils": "npm:^10.3.0" + "@types/ws": "npm:^8.0.0" + isomorphic-ws: "npm:^5.0.0" + tslib: "npm:^2.4.0" + ws: "npm:^8.17.1" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/b7bfe6c1e2229beef35fb175102d0a5ab45e679834c45d579c3d0efe8b7d1e22836d3773e03b0c920bbc94a58d490d54d62f1484e043eef6a54cc426b4b92f98 + languageName: node + linkType: hard + +"@graphql-tools/executor@npm:^1.2.8": + version: 1.2.8 + resolution: "@graphql-tools/executor@npm:1.2.8" + dependencies: + "@graphql-tools/utils": "npm:^10.2.3" + "@graphql-typed-document-node/core": "npm:3.2.0" + "@repeaterjs/repeater": "npm:^3.0.4" + tslib: "npm:^2.4.0" + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/d51f3b830bf3bcb84f06ca1b90324ae36a468df68e595699d4856615d3309e76fa6ff6aeb90584572cc7f1c7c18e9fffa3ccc30d8d1e40c94de3488da80899df + languageName: node + linkType: hard + +"@graphql-tools/git-loader@npm:^8.0.0": + version: 8.0.6 + resolution: "@graphql-tools/git-loader@npm:8.0.6" + dependencies: + "@graphql-tools/graphql-tag-pluck": "npm:8.3.1" + "@graphql-tools/utils": "npm:^10.0.13" + is-glob: "npm:4.0.3" + micromatch: "npm:^4.0.4" + tslib: "npm:^2.4.0" + unixify: "npm:^1.0.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/c5995bf6b96309e6603a51e71208d1f610d3f270a9b03ac537004ae1ec12d53368716debe77dbaf94a22a68144db2edfbff944211b1d17e755682fdd3eac67eb + languageName: node + linkType: hard + +"@graphql-tools/github-loader@npm:^8.0.0": + version: 8.0.1 + resolution: "@graphql-tools/github-loader@npm:8.0.1" + dependencies: + "@ardatan/sync-fetch": "npm:^0.0.1" + "@graphql-tools/executor-http": "npm:^1.0.9" + "@graphql-tools/graphql-tag-pluck": "npm:^8.0.0" + "@graphql-tools/utils": "npm:^10.0.13" + "@whatwg-node/fetch": "npm:^0.9.0" + tslib: "npm:^2.4.0" + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/d309e8330bafc4ef9796e7223f195e391011bf491f652857abb427faef59bc559228a0d288f6b765bb49a0cfeae1db055863a81bd49db9d720ae2c25622160f1 + languageName: node + linkType: hard + +"@graphql-tools/graphql-file-loader@npm:^8.0.0": + version: 8.0.1 + resolution: "@graphql-tools/graphql-file-loader@npm:8.0.1" + dependencies: + "@graphql-tools/import": "npm:7.0.1" + "@graphql-tools/utils": "npm:^10.0.13" + globby: "npm:^11.0.3" + tslib: "npm:^2.4.0" + unixify: "npm:^1.0.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/fceb035cacf481a03b242e3cca912c998b23ab3ce77b1f7b7ca05b3f3ff9f09d117aeefca2b219d09c3f920506e3780fa1efcba47fee9615cc63281e49ee2caa + languageName: node + linkType: hard + +"@graphql-tools/graphql-tag-pluck@npm:8.3.1, @graphql-tools/graphql-tag-pluck@npm:^8.0.0": + version: 8.3.1 + resolution: "@graphql-tools/graphql-tag-pluck@npm:8.3.1" + dependencies: + "@babel/core": "npm:^7.22.9" + "@babel/parser": "npm:^7.16.8" + "@babel/plugin-syntax-import-assertions": "npm:^7.20.0" + "@babel/traverse": "npm:^7.16.8" + "@babel/types": "npm:^7.16.8" + "@graphql-tools/utils": "npm:^10.0.13" + tslib: "npm:^2.4.0" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10/8d70bcdcd8cd279049183aca747d6c2ed7092a5cf0cf5916faac1ef37ffa74f0c245c2a3a3d3b9979d9dfdd4ca59257b4c5621db699d637b847a2c5e02f491c2 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/ec784009dece05632df0e01f6f1ab57f137736d16457c7e6a8faa745bfc0b4f9e85557bb8cb0ab738cd1f5387e7ab0b7b0d2d7316ccc19f183f5149fe1f8e066 languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1": - version: 4.10.0 - resolution: "@eslint-community/regexpp@npm:4.10.0" - checksum: 10/8c36169c815fc5d726078e8c71a5b592957ee60d08c6470f9ce0187c8046af1a00afbda0a065cc40ff18d5d83f82aed9793c6818f7304a74a7488dc9f3ecbd42 +"@graphql-tools/import@npm:7.0.1": + version: 7.0.1 + resolution: "@graphql-tools/import@npm:7.0.1" + dependencies: + "@graphql-tools/utils": "npm:^10.0.13" + resolve-from: "npm:5.0.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/ff578aad5e6f65728e658895e7e6be6866c0a713efe528cd420dd84c31a672ee6c6a6956bdff11c3cb2d3d56a90382d78868c2b90798577e6b087c08e3cf4d2b languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.4": - version: 2.1.4 - resolution: "@eslint/eslintrc@npm:2.1.4" +"@graphql-tools/json-file-loader@npm:^8.0.0": + version: 8.0.1 + resolution: "@graphql-tools/json-file-loader@npm:8.0.1" dependencies: - ajv: "npm:^6.12.4" - debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" - ignore: "npm:^5.2.0" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" - minimatch: "npm:^3.1.2" - strip-json-comments: "npm:^3.1.1" - checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 + "@graphql-tools/utils": "npm:^10.0.13" + globby: "npm:^11.0.3" + tslib: "npm:^2.4.0" + unixify: "npm:^1.0.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/db19f579d845d8ee41101c938f9921644939830df97447a797b3bffc59a914f9d987b0002df912b766fb7a623718367274d292db218424dca4ae00b1c927c613 languageName: node linkType: hard -"@eslint/js@npm:8.57.0": - version: 8.57.0 - resolution: "@eslint/js@npm:8.57.0" - checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 +"@graphql-tools/load@npm:^8.0.0": + version: 8.0.2 + resolution: "@graphql-tools/load@npm:8.0.2" + dependencies: + "@graphql-tools/schema": "npm:^10.0.3" + "@graphql-tools/utils": "npm:^10.0.13" + p-limit: "npm:3.1.0" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/ca291b6790ed34b9ec4ebb56fbebe0be208712b7d6d4cf54283a0d1bee65b49da7b2b254d6fc14e56fedd865270536551a2d09545ec91c6fa4d5097aa8f12aae languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": - version: 2.1.1 - resolution: "@fastify/busboy@npm:2.1.1" - checksum: 10/2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 +"@graphql-tools/merge@npm:^9.0.0, @graphql-tools/merge@npm:^9.0.3": + version: 9.0.4 + resolution: "@graphql-tools/merge@npm:9.0.4" + dependencies: + "@graphql-tools/utils": "npm:^10.0.13" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/9f60577237f43e66cabb2de01552a0231286b8a32877d0b5bcde31c2352867c63e55a236a3a1ae07e313f42ac070695a4f79ed6d04e6bbb8842e8e75d8276dd8 languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.0.0": +"@graphql-tools/optimize@npm:^2.0.0": version: 2.0.0 - resolution: "@formatjs/ecma402-abstract@npm:2.0.0" + resolution: "@graphql-tools/optimize@npm:2.0.0" dependencies: - "@formatjs/intl-localematcher": "npm:0.5.4" tslib: "npm:^2.4.0" - checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/7f79c0e1852abc571308e887d27d613da5b797256c8c6eb6c5fe7ca77f09e61524778ae281cebc0b698c51d4fe1074e2b8e0d0627b8e2dcf505aa6ed09b49a2f languageName: node linkType: hard -"@formatjs/fast-memoize@npm:2.2.0": - version: 2.2.0 - resolution: "@formatjs/fast-memoize@npm:2.2.0" +"@graphql-tools/prisma-loader@npm:^8.0.0": + version: 8.0.4 + resolution: "@graphql-tools/prisma-loader@npm:8.0.4" dependencies: + "@graphql-tools/url-loader": "npm:^8.0.2" + "@graphql-tools/utils": "npm:^10.0.13" + "@types/js-yaml": "npm:^4.0.0" + "@whatwg-node/fetch": "npm:^0.9.0" + chalk: "npm:^4.1.0" + debug: "npm:^4.3.1" + dotenv: "npm:^16.0.0" + graphql-request: "npm:^6.0.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.0" + jose: "npm:^5.0.0" + js-yaml: "npm:^4.0.0" + lodash: "npm:^4.17.20" + scuid: "npm:^1.1.0" tslib: "npm:^2.4.0" - checksum: 10/8697fe72a7ece252d600a7d08105f2a2f758e2dd96f54ac0a4c508b1205a559fc08835635e1f8e5ca9dcc3ee61ce1fca4a0e7047b402f29fc96051e293a280ff + yaml-ast-parser: "npm:^0.0.43" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/f0e20d1d7442ede2103bdb067f9fd9d551ee996e993be5caee33a39d85fda0eb7a02404d8fe6b12eb9ba5537a2a42fa7345dc48ab68e9debd8b3039acacd4f40 languageName: node linkType: hard -"@formatjs/icu-messageformat-parser@npm:2.7.8": - version: 2.7.8 - resolution: "@formatjs/icu-messageformat-parser@npm:2.7.8" +"@graphql-tools/relay-operation-optimizer@npm:^7.0.0": + version: 7.0.1 + resolution: "@graphql-tools/relay-operation-optimizer@npm:7.0.1" dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" - "@formatjs/icu-skeleton-parser": "npm:1.8.2" + "@ardatan/relay-compiler": "npm:12.0.0" + "@graphql-tools/utils": "npm:^10.0.13" tslib: "npm:^2.4.0" - checksum: 10/292fd36268ad84337c0e798fc73b58e8f3cf3f362cea031f710fd78053d29b420526ab766a95745e162ae4a11bf846bc2f7ae5c2c0a3288d3bc9daa97a3be8c1 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/24a29aea91a0028d7624ae38fed1bfd387ecf5a0e297dbbca23c0f73c51956765db7517b2a75a5b3a6003ea5e3f025f0fc4f527eec22b05e7e25216dc6318797 languageName: node linkType: hard -"@formatjs/icu-skeleton-parser@npm:1.8.2": - version: 1.8.2 - resolution: "@formatjs/icu-skeleton-parser@npm:1.8.2" +"@graphql-tools/schema@npm:^10.0.0, @graphql-tools/schema@npm:^10.0.3, @graphql-tools/schema@npm:^10.0.4": + version: 10.0.4 + resolution: "@graphql-tools/schema@npm:10.0.4" dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" + "@graphql-tools/merge": "npm:^9.0.3" + "@graphql-tools/utils": "npm:^10.2.1" tslib: "npm:^2.4.0" - checksum: 10/a06b61cf6c298bbbc23349e391bad8a1cf0a6a32dc4928a4681a3aa6f38dd8c6a181dc4067e228f67584d4dc181d862704095e65c38cfac077c984dc24ba54d3 + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/c2144949d98096c7f51ac0ecd9979d525e5b1b0f6b31c87961277fcefc82286ad9a9f55e93c2c3fd36745a1ef9ac341ec395d0db2c071a55a2a228ab563c953a languageName: node linkType: hard -"@formatjs/intl-displaynames@npm:6.6.8": - version: 6.6.8 - resolution: "@formatjs/intl-displaynames@npm:6.6.8" - dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" - "@formatjs/intl-localematcher": "npm:0.5.4" +"@graphql-tools/url-loader@npm:^8.0.0, @graphql-tools/url-loader@npm:^8.0.2": + version: 8.0.2 + resolution: "@graphql-tools/url-loader@npm:8.0.2" + dependencies: + "@ardatan/sync-fetch": "npm:^0.0.1" + "@graphql-tools/delegate": "npm:^10.0.4" + "@graphql-tools/executor-graphql-ws": "npm:^1.1.2" + "@graphql-tools/executor-http": "npm:^1.0.9" + "@graphql-tools/executor-legacy-ws": "npm:^1.0.6" + "@graphql-tools/utils": "npm:^10.0.13" + "@graphql-tools/wrap": "npm:^10.0.2" + "@types/ws": "npm:^8.0.0" + "@whatwg-node/fetch": "npm:^0.9.0" + isomorphic-ws: "npm:^5.0.0" tslib: "npm:^2.4.0" - checksum: 10/d305787fe2a35f9725b1293f70a2ccc454310c5df48ad62613d674a648dd318bec2a3ccd8656047443d1e438bb1cf8140e354d9c4a4101ec38df00614e0244b7 + value-or-promise: "npm:^1.0.11" + ws: "npm:^8.12.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/0b5c7a5593ef33ae64f83fb3458c0e87525e16b06280408a4b1eca1f44b493d09b97052dc5059a5327fe8a98f91a22ccdf0990264e577d18243b489f1994d0ca languageName: node linkType: hard -"@formatjs/intl-listformat@npm:7.5.7": - version: 7.5.7 - resolution: "@formatjs/intl-listformat@npm:7.5.7" +"@graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.13, @graphql-tools/utils@npm:^10.1.1, @graphql-tools/utils@npm:^10.2.1, @graphql-tools/utils@npm:^10.2.3, @graphql-tools/utils@npm:^10.3.0, @graphql-tools/utils@npm:^10.3.2": + version: 10.3.2 + resolution: "@graphql-tools/utils@npm:10.3.2" dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" - "@formatjs/intl-localematcher": "npm:0.5.4" + "@graphql-typed-document-node/core": "npm:^3.1.1" + cross-inspect: "npm:1.0.0" + dset: "npm:^3.1.2" tslib: "npm:^2.4.0" - checksum: 10/ba7e1ee9c29f2a8cfc66d51c5f273f279ced048b427533aa3d502cf7d12d510e79965d0471a158fd41d7cc7314f688c56d863c3ebec8e5e550d687f64d210794 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/37c4d11732fa470bbea3c8c9962837155bd0d22f5524a246e35d499872ca2db78acdaa515965564b4062be11d5f347e054724d6307d7bff78cd1c28885e2e371 languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.5.4": - version: 0.5.4 - resolution: "@formatjs/intl-localematcher@npm:0.5.4" +"@graphql-tools/wrap@npm:^10.0.2": + version: 10.0.5 + resolution: "@graphql-tools/wrap@npm:10.0.5" dependencies: + "@graphql-tools/delegate": "npm:^10.0.4" + "@graphql-tools/schema": "npm:^10.0.3" + "@graphql-tools/utils": "npm:^10.1.1" tslib: "npm:^2.4.0" - checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778 + value-or-promise: "npm:^1.0.12" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/02d5278bf1aea75897850f9bcf691104979490d5e74b577c9dc64df2c920d4be43fa8dbdedb7a58444d8baa96cf475939f2e5cc696ab60df16a0b04bf09c9219 languageName: node linkType: hard -"@formatjs/intl@npm:2.10.4": - version: 2.10.4 - resolution: "@formatjs/intl@npm:2.10.4" - dependencies: - "@formatjs/ecma402-abstract": "npm:2.0.0" - "@formatjs/fast-memoize": "npm:2.2.0" - "@formatjs/icu-messageformat-parser": "npm:2.7.8" - "@formatjs/intl-displaynames": "npm:6.6.8" - "@formatjs/intl-listformat": "npm:7.5.7" - intl-messageformat: "npm:10.5.14" - tslib: "npm:^2.4.0" +"@graphql-typed-document-node/core@npm:3.2.0, @graphql-typed-document-node/core@npm:^3.1.1, @graphql-typed-document-node/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" peerDependencies: - typescript: ^4.7 || 5 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/1d4e60cfb3edecb51a5f8dd2c2b1c784c1a9bfcbc1b6240323d8009e5f375e5c526c9eb19149b42e0e50f6a473e9709378071658ed25a62dc3923dd7fc6a2843 + 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 + checksum: 10/fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d languageName: node linkType: hard @@ -2627,6 +3546,13 @@ __metadata: languageName: node linkType: hard +"@kamilkisiela/fast-url-parser@npm:^1.1.4": + version: 1.1.4 + resolution: "@kamilkisiela/fast-url-parser@npm:1.1.4" + checksum: 10/5b79438235a81817b02b96ddc581c996961cec5b40c7d6ebabd01ac6af8d4a35a43b9b263144af25386cef92c054c3ef6b1723b09eb0d8cf7b4053781a474c5f + languageName: node + linkType: hard + "@lcdp/offline-plugin@npm:^5.0.7": version: 5.1.1 resolution: "@lcdp/offline-plugin@npm:5.1.1" @@ -2705,6 +3631,39 @@ __metadata: languageName: node linkType: hard +"@peculiar/asn1-schema@npm:^2.3.8": + version: 2.3.8 + resolution: "@peculiar/asn1-schema@npm:2.3.8" + dependencies: + asn1js: "npm:^3.0.5" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + checksum: 10/da349985cff73ae7ea52b6b66c6b4b339a768d5eb9164ad03e73c30985ec0a1c94849b323a826b00a049d7de3840368f77bebe84193205a77565cdfdac6ed524 + languageName: node + linkType: hard + +"@peculiar/json-schema@npm:^1.1.12": + version: 1.1.12 + resolution: "@peculiar/json-schema@npm:1.1.12" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10/dfec178afe63a02b6d45da8a18e51ef417e9f5412a8c2809c9a07b29b9376fadee1b4f2ea2d92d4e5a7b8eba76d9e99afbef6d7e9a27bd85257f69c4da228cbc + languageName: node + linkType: hard + +"@peculiar/webcrypto@npm:^1.4.0": + version: 1.5.0 + resolution: "@peculiar/webcrypto@npm:1.5.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/json-schema": "npm:^1.1.12" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + webcrypto-core: "npm:^1.8.0" + checksum: 10/a6658390c37b1d386f46066e796985eb56f6f86a772e1373c364ec9a8257adf8623f156596613d2828b489e2b5f32f9d2b0820289b4981646001cba7d21ae2f6 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -2790,6 +3749,13 @@ __metadata: languageName: node linkType: hard +"@repeaterjs/repeater@npm:^3.0.4": + version: 3.0.6 + resolution: "@repeaterjs/repeater@npm:3.0.6" + checksum: 10/25698e822847b776006428f31e2d31fbcb4faccf30c1c8d68d6e1308e58b49afb08764d1dd15536ddd67775cd01fd6c2fb22f039c05a71865448fbcfb2246af2 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -3070,6 +4036,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4.0.0": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: 10/a0ce595db8a987904badd21fc50f9f444cb73069f4b95a76cc222e0a17b3ff180669059c763ec314bc4c3ce284379177a9da80e83c5f650c6c1310cafbfaa8e6 + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -3250,6 +4223,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.0.0": + version: 8.5.11 + resolution: "@types/ws@npm:8.5.11" + dependencies: + "@types/node": "npm:*" + checksum: 10/950d13b762fc7c092a0fc1450c41229a1d41abb93cb72251068885bd46fa4bbcf461c00df2e77de3f7a547371998b650a720ed90417562af0772b14a8a009dec + languageName: node + linkType: hard + "@types/ws@npm:^8.5.10": version: 8.5.10 resolution: "@types/ws@npm:8.5.10" @@ -3400,6 +4382,16 @@ __metadata: languageName: node linkType: hard +"@urql/core@npm:^5.0.4": + version: 5.0.4 + resolution: "@urql/core@npm:5.0.4" + dependencies: + "@0no-co/graphql.web": "npm:^1.0.5" + wonka: "npm:^6.3.2" + checksum: 10/fe3ee871bde8ee8931d1f791a1475f8bf97940eb93eaa690b11dbfcfa9409644ea87c204245f64e22ea1ddeb16a55e7532b765c18eedcdd9ae8032981889d06a + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": version: 1.12.1 resolution: "@webassemblyjs/ast@npm:1.12.1" @@ -3584,6 +4576,69 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/events@npm:^0.0.3": + version: 0.0.3 + resolution: "@whatwg-node/events@npm:0.0.3" + checksum: 10/af26f40d4d0a0f5f0ee45fc6124afb8d6b33988dae96ab0fb87aa5e66d1ff08a749491b9da533ea524bbaebd4a770736f254d574a91ab4455386aa098cee8c77 + languageName: node + linkType: hard + +"@whatwg-node/events@npm:^0.1.0": + version: 0.1.1 + resolution: "@whatwg-node/events@npm:0.1.1" + checksum: 10/3a356ca23522190201e27446cfd7ebf1cf96815ddb9d1ba5da0a00bbe6c1d28b4094862104411101fbedd47c758b25fe3683033f6a3e80933029efd664c33567 + languageName: node + linkType: hard + +"@whatwg-node/fetch@npm:^0.8.0": + version: 0.8.8 + resolution: "@whatwg-node/fetch@npm:0.8.8" + dependencies: + "@peculiar/webcrypto": "npm:^1.4.0" + "@whatwg-node/node-fetch": "npm:^0.3.6" + busboy: "npm:^1.6.0" + urlpattern-polyfill: "npm:^8.0.0" + web-streams-polyfill: "npm:^3.2.1" + checksum: 10/4d04f28a3db1886a5ab6070af0d8d6b90c891596495e62417aa296dcdf65506703fb5f76937f7a7b7f4125721ef80f4ac9204a948588c33517dc064c746d7a42 + languageName: node + linkType: hard + +"@whatwg-node/fetch@npm:^0.9.0": + version: 0.9.18 + resolution: "@whatwg-node/fetch@npm:0.9.18" + dependencies: + "@whatwg-node/node-fetch": "npm:^0.5.7" + urlpattern-polyfill: "npm:^10.0.0" + checksum: 10/3ef78969991a44ea99c4f88929c1472b65f066c9d2734992d0de77d11089eec5c3477503cd9513201516056496e28c9fb172635f69867417a0c68e88f4409c96 + languageName: node + linkType: hard + +"@whatwg-node/node-fetch@npm:^0.3.6": + version: 0.3.6 + resolution: "@whatwg-node/node-fetch@npm:0.3.6" + dependencies: + "@whatwg-node/events": "npm:^0.0.3" + busboy: "npm:^1.6.0" + fast-querystring: "npm:^1.1.1" + fast-url-parser: "npm:^1.1.3" + tslib: "npm:^2.3.1" + checksum: 10/8284e385cf50f4479f19a5be8feb0d55f448af3bb7a62ec654ec9e4232ce3f0858191494f508f5196a94b16017d5e08f8e0bce9f49af4dc133a39d5047b8e369 + languageName: node + linkType: hard + +"@whatwg-node/node-fetch@npm:^0.5.7": + version: 0.5.11 + resolution: "@whatwg-node/node-fetch@npm:0.5.11" + dependencies: + "@kamilkisiela/fast-url-parser": "npm:^1.1.4" + "@whatwg-node/events": "npm:^0.1.0" + busboy: "npm:^1.6.0" + fast-querystring: "npm:^1.1.1" + tslib: "npm:^2.3.1" + checksum: 10/0ddbe236ce38b7859ad4b3c2097560585473d6d70afd5e72d88a97daa89176af7b888c95b6fc93a0bd6a471a42b7fb32ef8dac4d1558fbd6d835b25887329a91 + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -3728,7 +4783,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.3.2": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -3864,6 +4919,31 @@ __metadata: languageName: node linkType: hard +"asap@npm:~2.0.3": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda + languageName: node + linkType: hard + +"asn1js@npm:^3.0.1, asn1js@npm:^3.0.5": + version: 3.0.5 + resolution: "asn1js@npm:3.0.5" + dependencies: + pvtsutils: "npm:^1.3.2" + pvutils: "npm:^1.1.3" + tslib: "npm:^2.4.0" + checksum: 10/17fb0302432186631550de9606a4622ec366646d072cde9cdf4bcafa47bd2425e157eeb7b1377ee6520f8b46687b4ecaee31cf0ad2fa494361a1938b2ed53194 + languageName: node + linkType: hard + +"astral-regex@npm:^2.0.0": + version: 2.0.0 + resolution: "astral-regex@npm:2.0.0" + checksum: 10/876231688c66400473ba505731df37ea436e574dd524520294cc3bbc54ea40334865e01fa0d074d74d036ee874ee7e62f486ea38bc421ee8e6a871c06f011766 + languageName: node + linkType: hard + "async@npm:^3.2.3": version: 3.2.5 resolution: "async@npm:3.2.5" @@ -3885,6 +4965,13 @@ __metadata: languageName: node linkType: hard +"auto-bind@npm:~4.0.0": + version: 4.0.0 + resolution: "auto-bind@npm:4.0.0" + checksum: 10/00cad71cce5742faccb7dd65c1b55ebc4f45add4b0c9a1547b10b05bab22813230133b0c892c67ba3eb969a4524710c5e43cc45c72898ec84e56f3a596e7a04f + languageName: node + linkType: hard + "autoprefixer@npm:^10.4.19": version: 10.4.19 resolution: "autoprefixer@npm:10.4.19" @@ -3963,6 +5050,50 @@ __metadata: languageName: node linkType: hard +"babel-plugin-syntax-trailing-function-commas@npm:^7.0.0-beta.0": + version: 7.0.0-beta.0 + resolution: "babel-plugin-syntax-trailing-function-commas@npm:7.0.0-beta.0" + checksum: 10/e37509156ca945dd9e4b82c66dd74f2d842ad917bd280cb5aa67960942300cd065eeac476d2514bdcdedec071277a358f6d517c31d9f9244d9bbc3619a8ecf8a + languageName: node + linkType: hard + +"babel-preset-fbjs@npm:^3.4.0": + version: 3.4.0 + resolution: "babel-preset-fbjs@npm:3.4.0" + dependencies: + "@babel/plugin-proposal-class-properties": "npm:^7.0.0" + "@babel/plugin-proposal-object-rest-spread": "npm:^7.0.0" + "@babel/plugin-syntax-class-properties": "npm:^7.0.0" + "@babel/plugin-syntax-flow": "npm:^7.0.0" + "@babel/plugin-syntax-jsx": "npm:^7.0.0" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.0.0" + "@babel/plugin-transform-arrow-functions": "npm:^7.0.0" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.0.0" + "@babel/plugin-transform-block-scoping": "npm:^7.0.0" + "@babel/plugin-transform-classes": "npm:^7.0.0" + "@babel/plugin-transform-computed-properties": "npm:^7.0.0" + "@babel/plugin-transform-destructuring": "npm:^7.0.0" + "@babel/plugin-transform-flow-strip-types": "npm:^7.0.0" + "@babel/plugin-transform-for-of": "npm:^7.0.0" + "@babel/plugin-transform-function-name": "npm:^7.0.0" + "@babel/plugin-transform-literals": "npm:^7.0.0" + "@babel/plugin-transform-member-expression-literals": "npm:^7.0.0" + "@babel/plugin-transform-modules-commonjs": "npm:^7.0.0" + "@babel/plugin-transform-object-super": "npm:^7.0.0" + "@babel/plugin-transform-parameters": "npm:^7.0.0" + "@babel/plugin-transform-property-literals": "npm:^7.0.0" + "@babel/plugin-transform-react-display-name": "npm:^7.0.0" + "@babel/plugin-transform-react-jsx": "npm:^7.0.0" + "@babel/plugin-transform-shorthand-properties": "npm:^7.0.0" + "@babel/plugin-transform-spread": "npm:^7.0.0" + "@babel/plugin-transform-template-literals": "npm:^7.0.0" + babel-plugin-syntax-trailing-function-commas: "npm:^7.0.0-beta.0" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/1e73ebaaeac805aad15793d06a40a63be096730f58708ec434f08578b5ccba890190cda8fdf1c626ab081a8e1cfd376c9db82eaf78a0fafdbcc2362eb2963804 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -4092,7 +5223,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": +"braces@npm:^3.0.2, braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -4115,6 +5246,29 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.23.1": + version: 4.23.2 + resolution: "browserslist@npm:4.23.2" + dependencies: + caniuse-lite: "npm:^1.0.30001640" + electron-to-chromium: "npm:^1.4.820" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.1.0" + bin: + browserslist: cli.js + checksum: 10/326a98b1c39bcc9a99b197f15790dc28e122b1aead3257c837421899377ac96239123f26868698085b3d9be916d72540602738e1f857e86a387e810af3fda6e5 + languageName: node + linkType: hard + +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: "npm:^0.4.0" + checksum: 10/edba1b65bae682450be4117b695997972bd9a3c4dfee029cab5bcb72ae5393a79a8f909b8bc77957eb0deec1c7168670f18f4d5c556f46cdd3bca5f3b3a8d020 + languageName: node + linkType: hard + "buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -4155,6 +5309,15 @@ __metadata: languageName: node linkType: hard +"busboy@npm:^1.6.0": + version: 1.6.0 + resolution: "busboy@npm:1.6.0" + dependencies: + streamsearch: "npm:^1.1.0" + checksum: 10/bee10fa10ea58e7e3e7489ffe4bda6eacd540a17de9f9cd21cc37e297b2dd9fe52b2715a5841afaec82900750d810d01d7edb4b2d456427f449b92b417579763 + languageName: node + linkType: hard + "bytes@npm:3.0.0": version: 3.0.0 resolution: "bytes@npm:3.0.0" @@ -4226,6 +5389,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^5.0.0": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + "caniuse-api@npm:^3.0.0": version: 3.0.0 resolution: "caniuse-api@npm:3.0.0" @@ -4245,6 +5415,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001640": + version: 1.0.30001642 + resolution: "caniuse-lite@npm:1.0.30001642" + checksum: 10/8d80ea82be453ae0fdfea8766d82740a4945c1b99189650f29bfc458d4e235d7e99027a8f8bc5a4228d8c4457ba896315284b0703f300353ad5f09d8e693de10 + languageName: node + linkType: hard + "capital-case@npm:^1.0.4": version: 1.0.4 resolution: "capital-case@npm:1.0.4" @@ -4277,7 +5454,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4294,6 +5471,24 @@ __metadata: languageName: node linkType: hard +"change-case-all@npm:1.0.15": + version: 1.0.15 + resolution: "change-case-all@npm:1.0.15" + dependencies: + change-case: "npm:^4.1.2" + is-lower-case: "npm:^2.0.2" + is-upper-case: "npm:^2.0.2" + lower-case: "npm:^2.0.2" + lower-case-first: "npm:^2.0.2" + sponge-case: "npm:^1.0.1" + swap-case: "npm:^2.0.2" + title-case: "npm:^3.0.3" + upper-case: "npm:^2.0.2" + upper-case-first: "npm:^2.0.2" + checksum: 10/e1dabdcd8447a3690f3faf15f92979dfbc113109b50916976e1d5e518e6cfdebee4f05f54d0ca24fb79a4bf835185b59ae25e967bb3dc10bd236a775b19ecc52 + languageName: node + linkType: hard + "change-case@npm:^4.1.2": version: 4.1.2 resolution: "change-case@npm:4.1.2" @@ -4431,6 +5626,23 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-truncate@npm:2.1.0" + dependencies: + slice-ansi: "npm:^3.0.0" + string-width: "npm:^4.2.0" + checksum: 10/976f1887de067a8cd6ec830a7a8508336aebe6cec79b521d98ed13f67ef073b637f7305675b6247dd22f9e9cf045ec55fe746c7bdb288fbe8db0dfdc9fd52e55 + languageName: node + linkType: hard + +"cli-width@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-width@npm:3.0.0" + checksum: 10/8730848b04fb189666ab037a35888d191c8f05b630b1d770b0b0e4c920b47bb5cc14bddf6b8ffe5bfc66cee97c8211d4d18e756c1ffcc75d7dbe7e1186cd7826 + languageName: node + linkType: hard + "cli-width@npm:^4.1.0": version: 4.1.0 resolution: "cli-width@npm:4.1.0" @@ -4438,14 +5650,36 @@ __metadata: languageName: node linkType: hard -"clipboardy@npm:4.0.0": - version: 4.0.0 - resolution: "clipboardy@npm:4.0.0" +"clipboardy@npm:4.0.0": + version: 4.0.0 + resolution: "clipboardy@npm:4.0.0" + dependencies: + execa: "npm:^8.0.1" + is-wsl: "npm:^3.1.0" + is64bit: "npm:^2.0.0" + checksum: 10/ec4ebe7e5c81d9c9cb994637e7b0e068c1c8fc272167ecd5519f967427271ec66e0e64da7268a2630b860eff42933aeabe25ba5e42bb80dbf1fae6362df059ed + languageName: node + linkType: hard + +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10/44afbcc29df0899e87595590792a871cd8c4bc7d6ce92832d9ae268d141a77022adafca1aeaeccff618b62a613b8354e57fe22a275c199ec04baf00d381ef6ab + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" dependencies: - execa: "npm:^8.0.1" - is-wsl: "npm:^3.1.0" - is64bit: "npm:^2.0.0" - checksum: 10/ec4ebe7e5c81d9c9cb994637e7b0e068c1c8fc272167ecd5519f967427271ec66e0e64da7268a2630b860eff42933aeabe25ba5e42bb80dbf1fae6362df059ed + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 languageName: node linkType: hard @@ -4526,7 +5760,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.14": +"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.16": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f @@ -4577,6 +5811,13 @@ __metadata: languageName: node linkType: hard +"common-tags@npm:1.8.2": + version: 1.8.2 + resolution: "common-tags@npm:1.8.2" + checksum: 10/c665d0f463ee79dda801471ad8da6cb33ff7332ba45609916a508ad3d77ba07ca9deeb452e83f81f24c2b081e2c1315347f23d239210e63d1c5e1a0c7c019fe2 + languageName: node + linkType: hard + "compressible@npm:~2.0.16": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -4709,7 +5950,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^8.2.0": +"cosmiconfig@npm:^8.1.0, cosmiconfig@npm:^8.1.3, cosmiconfig@npm:^8.2.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" dependencies: @@ -4743,6 +5984,24 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.5": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: "npm:^2.6.12" + checksum: 10/ac8c4ca87d2ac0e17a19b6a293a67ee8934881aee5ec9a5a8323c30e9a9a60a0f5291d3c0d633ec2a2f970cbc60978d628804dfaf03add92d7e720b6d37f392c + languageName: node + linkType: hard + +"cross-inspect@npm:1.0.0": + version: 1.0.0 + resolution: "cross-inspect@npm:1.0.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/975c81799549627027254eb70f1c349cefb14435d580bea6f351f510c839dcb1a9288983407bac2ad317e6eff29cf1e99299606da21f404562bfa64cec502239 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -5098,6 +6357,13 @@ __metadata: languageName: node linkType: hard +"dataloader@npm:^2.2.2": + version: 2.2.2 + resolution: "dataloader@npm:2.2.2" + checksum: 10/9c7a1f02cfa6391ab8bc21ebd0ef60b03832bd3beafdfecf48b111fba14090f98d33965f8e268045ba3c289f801b6a9000a9e61a41188363bdee2344811f64f1 + languageName: node + linkType: hard + "date-fns-tz@npm:^2.0.0": version: 2.0.1 resolution: "date-fns-tz@npm:2.0.1" @@ -5132,6 +6398,8 @@ __metadata: "@blueprintjs/datetime2": "npm:^2.3.5" "@blueprintjs/icons": "npm:^5.1.0" "@blueprintjs/select": "npm:^5.1.5" + "@graphql-codegen/cli": "npm:^5.0.2" + "@graphql-codegen/client-preset": "npm:^4.3.2" "@lcdp/offline-plugin": "npm:^5.0.7" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.15" "@reduxjs/toolkit": "npm:^2.2.5" @@ -5144,6 +6412,7 @@ __metadata: "@types/react-dom": "npm:^18.3.0" "@typescript-eslint/eslint-plugin": "npm:^7.13.0" "@typescript-eslint/parser": "npm:^7.13.0" + "@urql/core": "npm:^5.0.4" autoprefixer: "npm:^10.4.19" axios: "npm:^1.7.2" babel-loader: "npm:^9.1.3" @@ -5156,6 +6425,7 @@ __metadata: css-loader: "npm:^7.1.2" css-minimizer-webpack-plugin: "npm:^7.0.0" date-fns: "npm:^2.28.0" + dotenv: "npm:^16.4.5" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" eslint-plugin-prettier: "npm:^5.1.3" @@ -5164,6 +6434,7 @@ __metadata: favicons-webpack-plugin: "npm:^6.0.0" fork-ts-checker-webpack-plugin: "npm:^9.0.2" fuzzy-search: "npm:^3.0.2" + graphql: "npm:^16.9.0" he: "npm:^1.2.0" html-entities: "npm:^2.5.2" html-loader: "npm:^5.0.0" @@ -5210,6 +6481,13 @@ __metadata: languageName: unknown linkType: soft +"debounce@npm:^1.2.0": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 10/0b95b2a9d80ed69117d890f8dab8c0f2d6066f8d20edd1d810ae51f8f366a6d4c8b1d56e97dcb9304e93d57de4d5db440d34a03def7dad50403fc3f22bf16808 + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -5231,6 +6509,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10/ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decimal.js@npm:^10.4.3": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" @@ -5380,6 +6665,13 @@ __metadata: languageName: node linkType: hard +"dependency-graph@npm:^0.11.0": + version: 0.11.0 + resolution: "dependency-graph@npm:0.11.0" + checksum: 10/6b5eb540303753037a613e781da4b81534d139cbabc92f342630ed622e3ef4c332fc40cf87823e1ec71a7aeb4b195f8d88d7e625931ce6007bf2bf09a8bfb01e + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -5387,6 +6679,13 @@ __metadata: languageName: node linkType: hard +"detect-indent@npm:^6.0.0": + version: 6.1.0 + resolution: "detect-indent@npm:6.1.0" + checksum: 10/ab953a73c72dbd4e8fc68e4ed4bfd92c97eb6c43734af3900add963fd3a9316f3bc0578b018b24198d4c31a358571eff5f0656e81a1f3b9ad5c547d58b2d093d + languageName: node + linkType: hard + "detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.2": version: 2.0.2 resolution: "detect-libc@npm:2.0.2" @@ -5533,6 +6832,20 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.0.0, dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 + languageName: node + linkType: hard + +"dset@npm:^3.1.2": + version: 3.1.3 + resolution: "dset@npm:3.1.3" + checksum: 10/f3f7096718eeabe1608886364ea02254d5221a4d59d4fb4d2fd2fdf53cccf293d486793a44c894d3a07a916a283d1214e831e423839096d461a38571fc092126 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -5565,6 +6878,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.820": + version: 1.4.827 + resolution: "electron-to-chromium@npm:1.4.827" + checksum: 10/7fa44aeebc5548874d33e417579d998d8e9a3d7b07fae22429ee7de5866c73b3158d56969146df3dcf44a222dcd91972ee786d0427f461e0c98bff79e408e782 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -5831,6 +7151,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.2": + version: 3.1.2 + resolution: "escalade@npm:3.1.2" + checksum: 10/a1e07fea2f15663c30e40b9193d658397846ffe28ce0a3e4da0d8e485fedfeca228ab846aee101a05015829adf39f9934ff45b2a3fca47bed37a29646bd05cd3 + languageName: node + linkType: hard + "escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -6191,7 +7518,7 @@ __metadata: languageName: node linkType: hard -"external-editor@npm:^3.1.0": +"external-editor@npm:^3.0.3, external-editor@npm:^3.1.0": version: 3.1.0 resolution: "external-editor@npm:3.1.0" dependencies: @@ -6202,6 +7529,20 @@ __metadata: languageName: node linkType: hard +"extract-files@npm:^11.0.0": + version: 11.0.0 + resolution: "extract-files@npm:11.0.0" + checksum: 10/02bf0dde9617d67795e38a182d8bf58828a7c5d77762623ff05e72d461a0e980071a860e2503231db2cc8824d8da35cefb1750937dcbe018cb0e67e37f20a7be + languageName: node + linkType: hard + +"fast-decode-uri-component@npm:^1.0.1": + version: 1.0.1 + resolution: "fast-decode-uri-component@npm:1.0.1" + checksum: 10/4b6ed26974414f688be4a15eab6afa997bad4a7c8605cb1deb928b28514817b4523a1af0fa06621c6cbfedb7e5615144c2c3e7512860e3a333a31a28d537dca7 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6243,6 +7584,24 @@ __metadata: languageName: node linkType: hard +"fast-querystring@npm:^1.1.1": + version: 1.1.2 + resolution: "fast-querystring@npm:1.1.2" + dependencies: + fast-decode-uri-component: "npm:^1.0.1" + checksum: 10/981da9b914f2b639dc915bdfa4f34ab028b967d428f02fbd293d99258593fde69c48eea73dfa03ced088268e0a8045c642e8debcd9b4821ebd125e130a0430c7 + languageName: node + linkType: hard + +"fast-url-parser@npm:^1.1.3": + version: 1.1.3 + resolution: "fast-url-parser@npm:1.1.3" + dependencies: + punycode: "npm:^1.3.2" + checksum: 10/6d33f46ce9776f7f3017576926207a950ca39bc5eb78fc794404f2288fe494720f9a119084b75569bd9eb09d2b46678bfaf39c191fb2c808ef3c833dc8982752 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.12": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -6297,6 +7656,37 @@ __metadata: languageName: node linkType: hard +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: "npm:2.1.1" + checksum: 10/4f95d336fb805786759e383fd7fff342ceb7680f53efcc0ef82f502eb479ce35b98e8b207b6dfdfeea0eba845862107dc73813775fc6b56b3098c6e90a2dad77 + languageName: node + linkType: hard + +"fbjs-css-vars@npm:^1.0.0": + version: 1.0.2 + resolution: "fbjs-css-vars@npm:1.0.2" + checksum: 10/72baf6d22c45b75109118b4daecb6c8016d4c83c8c0f23f683f22e9d7c21f32fff6201d288df46eb561e3c7d4bb4489b8ad140b7f56444c453ba407e8bd28511 + languageName: node + linkType: hard + +"fbjs@npm:^3.0.0": + version: 3.0.5 + resolution: "fbjs@npm:3.0.5" + dependencies: + cross-fetch: "npm:^3.1.5" + fbjs-css-vars: "npm:^1.0.0" + loose-envify: "npm:^1.0.0" + object-assign: "npm:^4.1.0" + promise: "npm:^7.1.1" + setimmediate: "npm:^1.0.5" + ua-parser-js: "npm:^1.0.35" + checksum: 10/71252595b00b06fb0475a295c74d81ada1cc499b7e11f2cde51fef04618affa568f5b7f4927f61720c23254b9144be28f8acb2086a5001cf65df8eec87c6ca5c + languageName: node + linkType: hard + "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.2.0 resolution: "fetch-blob@npm:3.2.0" @@ -6307,6 +7697,15 @@ __metadata: languageName: node linkType: hard +"figures@npm:^3.0.0": + version: 3.2.0 + resolution: "figures@npm:3.2.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + checksum: 10/a3bf94e001be51d3770500789157f067218d4bc681a65e1f69d482de15120bcac822dceb1a7b3803f32e4e3a61a46df44f7f2c8ba95d6375e7491502e0dd3d97 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -6384,7 +7783,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.0.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -6615,6 +8014,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": version: 1.2.4 resolution: "get-intrinsic@npm:1.2.4" @@ -6709,7 +8115,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.3, glob@npm:^7.1.3": +"glob@npm:^7.0.3, glob@npm:^7.1.1, glob@npm:^7.1.3": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -6749,7 +8155,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": +"globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -6813,6 +8219,70 @@ __metadata: languageName: node linkType: hard +"graphql-config@npm:^5.0.2": + version: 5.0.3 + resolution: "graphql-config@npm:5.0.3" + dependencies: + "@graphql-tools/graphql-file-loader": "npm:^8.0.0" + "@graphql-tools/json-file-loader": "npm:^8.0.0" + "@graphql-tools/load": "npm:^8.0.0" + "@graphql-tools/merge": "npm:^9.0.0" + "@graphql-tools/url-loader": "npm:^8.0.0" + "@graphql-tools/utils": "npm:^10.0.0" + cosmiconfig: "npm:^8.1.0" + jiti: "npm:^1.18.2" + minimatch: "npm:^4.2.3" + string-env-interpolation: "npm:^1.0.1" + tslib: "npm:^2.4.0" + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + checksum: 10/be7667ea260c7db3e8b02c0d73d2a2bc214683d91886f883c73465e07aa204f9ae6bff494eaa253def31abd2bbe59e78c6b418ed456e06d2274050dbc45e33e7 + languageName: node + linkType: hard + +"graphql-request@npm:^6.0.0": + version: 6.1.0 + resolution: "graphql-request@npm:6.1.0" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.2.0" + cross-fetch: "npm:^3.1.5" + peerDependencies: + graphql: 14 - 16 + checksum: 10/a9c6f2eeaad972cdecb91437c15c785a282263fd0ef36f6fc5648e0945da488cdc10ab4736891ee1fbb928c7bf6e0bc8e0284df514254adefe02cc406ba5fce5 + languageName: node + linkType: hard + +"graphql-tag@npm:^2.11.0": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: "npm:^2.1.0" + 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 + checksum: 10/23a2bc1d3fbeae86444204e0ac08522e09dc369559ba75768e47421a7321b59f352fb5b2c9a5c37d3cf6de890dca4e5ac47e740c7cc622e728572ecaa649089e + languageName: node + linkType: hard + +"graphql-ws@npm:^5.14.0": + version: 5.16.0 + resolution: "graphql-ws@npm:5.16.0" + peerDependencies: + graphql: ">=0.11 <=16" + checksum: 10/e56d903920c78fa88966e31940d281f8b35ef8c9f4543255bfe349e47a0e972c6ca746bcb52040b1c6938d22e42560228994399972abc473cfa6bcd183aca709 + languageName: node + linkType: hard + +"graphql@npm:^16.9.0": + version: 16.9.0 + resolution: "graphql@npm:16.9.0" + checksum: 10/5833f82bb6c31bec120bbf9cd400eda873e1bb7ef5c17974fa262cd82dc68728fda5d4cb859dc8aaa4c4fe4f6fe1103a9c47efc01a12c02ae5cb581d8e4029e2 + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -7086,6 +8556,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.0": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10/6679d46159ab3f9a5509ee80c3a3fc83fba3a920a5e18d32176c3327852c3c00ad640c0c4210a8fd70ea3c4a6d3a1b375bf01942516e7df80e2646bdc77658ab + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.4": version: 7.0.4 resolution: "https-proxy-agent@npm:7.0.4" @@ -7167,6 +8647,13 @@ __metadata: languageName: node linkType: hard +"immutable@npm:~3.7.6": + version: 3.7.6 + resolution: "immutable@npm:3.7.6" + checksum: 10/4f2cc2e0b6839befa2ea9d3ca478971a88ca78cb66c2b077416e5d5203f8e168bffb78284dd45fe1b427a4a8ac37194dfa3cd3e50b39529a00cca387bd6ac955 + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -7177,6 +8664,13 @@ __metadata: languageName: node linkType: hard +"import-from@npm:4.0.0": + version: 4.0.0 + resolution: "import-from@npm:4.0.0" + checksum: 10/1fa29c05b048da18914e91d9a529e5d9b91774bebbfab10e53f59bcc1667917672b971cf102fee857f142e5e433ce69fa1f0a596e1c7d82f9947a5ec352694b9 + languageName: node + linkType: hard + "import-local@npm:^3.0.2": version: 3.1.0 resolution: "import-local@npm:3.1.0" @@ -7234,6 +8728,29 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^8.0.0": + version: 8.2.6 + resolution: "inquirer@npm:8.2.6" + dependencies: + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.1.1" + cli-cursor: "npm:^3.1.0" + cli-width: "npm:^3.0.0" + external-editor: "npm:^3.0.3" + figures: "npm:^3.0.0" + lodash: "npm:^4.17.21" + mute-stream: "npm:0.0.8" + ora: "npm:^5.4.1" + run-async: "npm:^2.4.0" + rxjs: "npm:^7.5.5" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + through: "npm:^2.3.6" + wrap-ansi: "npm:^6.0.1" + checksum: 10/f642b9e5a94faaba54f277bdda2af0e0a6b592bd7f88c60e1614b5795b19336c7025e0c2923915d5f494f600a02fe8517413779a794415bb79a9563b061d68ab + languageName: node + linkType: hard + "inquirer@npm:^9.2.23": version: 9.2.23 resolution: "inquirer@npm:9.2.23" @@ -7283,6 +8800,15 @@ __metadata: languageName: node linkType: hard +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: "npm:^1.0.0" + checksum: 10/cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14 + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.1 resolution: "ip@npm:2.0.1" @@ -7304,6 +8830,16 @@ __metadata: languageName: node linkType: hard +"is-absolute@npm:^1.0.0": + version: 1.0.0 + resolution: "is-absolute@npm:1.0.0" + dependencies: + is-relative: "npm:^1.0.0" + is-windows: "npm:^1.0.1" + checksum: 10/9d16b2605eda3f3ce755410f1d423e327ad3a898bcb86c9354cf63970ed3f91ba85e9828aa56f5d6a952b9fae43d0477770f78d37409ae8ecc31e59ebc279b27 + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -7366,7 +8902,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": +"is-glob@npm:4.0.3, is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -7400,6 +8936,15 @@ __metadata: languageName: node linkType: hard +"is-lower-case@npm:^2.0.2": + version: 2.0.2 + resolution: "is-lower-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/ba57dd1201e15fd9b590654736afccf1b3b68e919f40c23ef13b00ebcc639b1d9c2f81fe86415bff3e8eccffec459786c9ac9dc8f3a19cfa4484206c411c1d7d + languageName: node + linkType: hard + "is-network-error@npm:^1.0.0": version: 1.0.1 resolution: "is-network-error@npm:1.0.1" @@ -7476,6 +9021,15 @@ __metadata: languageName: node linkType: hard +"is-relative@npm:^1.0.0": + version: 1.0.0 + resolution: "is-relative@npm:1.0.0" + dependencies: + is-unc-path: "npm:^1.0.0" + checksum: 10/3271a0df109302ef5e14a29dcd5d23d9788e15ade91a40b942b035827ffbb59f7ce9ff82d036ea798541a52913cbf9d2d0b66456340887b51f3542d57b5a4c05 + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -7490,6 +9044,15 @@ __metadata: languageName: node linkType: hard +"is-unc-path@npm:^1.0.0": + version: 1.0.0 + resolution: "is-unc-path@npm:1.0.0" + dependencies: + unc-path-regex: "npm:^0.1.2" + checksum: 10/e8abfde203f7409f5b03a5f1f8636e3a41e78b983702ef49d9343eb608cdfe691429398e8815157519b987b739bcfbc73ae7cf4c8582b0ab66add5171088eab6 + languageName: node + linkType: hard + "is-unicode-supported@npm:^0.1.0": version: 0.1.0 resolution: "is-unicode-supported@npm:0.1.0" @@ -7497,6 +9060,22 @@ __metadata: languageName: node linkType: hard +"is-upper-case@npm:^2.0.2": + version: 2.0.2 + resolution: "is-upper-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/cf4fd43c00c2e72cd5cff911923070b89f0933b464941bd782e2315385f80b5a5acd772db3b796542e5e3cfed735f4dffd88c54d62db1ebfc5c3daa7b1af2bc6 + languageName: node + linkType: hard + +"is-windows@npm:^1.0.1": + version: 1.0.2 + resolution: "is-windows@npm:1.0.2" + checksum: 10/438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 + languageName: node + linkType: hard + "is-wsl@npm:^3.1.0": version: 3.1.0 resolution: "is-wsl@npm:3.1.0" @@ -7553,6 +9132,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: 10/e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + "jackspeak@npm:^2.3.6": version: 2.3.6 resolution: "jackspeak@npm:2.3.6" @@ -7629,6 +9217,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.17.1, jiti@npm:^1.18.2": + version: 1.21.6 + resolution: "jiti@npm:1.21.6" + bin: + jiti: bin/jiti.js + checksum: 10/289b124cea411c130a14ffe88e3d38376ab44b6695616dfa0a1f32176a8f20ec90cdd6d2b9d81450fc6467cfa4d865f04f49b98452bff0f812bc400fd0ae78d6 + languageName: node + linkType: hard + "jiti@npm:^1.20.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" @@ -7638,6 +9235,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^5.0.0": + version: 5.6.3 + resolution: "jose@npm:5.6.3" + checksum: 10/829f07b8857221ada1cd112a5ebd084e34748f6e3247f5cb03aca0a1f25dd6a07283c9a4f5b854935bda505e3243405640550f8e9eb46c45b172dc4cb336ac89 + languageName: node + linkType: hard + "jotai@npm:^2.8.3": version: 2.8.3 resolution: "jotai@npm:2.8.3" @@ -7667,7 +9271,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": +"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -7795,6 +9399,16 @@ __metadata: languageName: node linkType: hard +"json-to-pretty-yaml@npm:^1.2.2": + version: 1.2.2 + resolution: "json-to-pretty-yaml@npm:1.2.2" + dependencies: + remedial: "npm:^1.0.7" + remove-trailing-spaces: "npm:^1.0.6" + checksum: 10/3ccd527c9a9cf41e123d75445605801dd0eebcddf53e00af05febc212a3657fceb03063399693d79cb2b7a8530dd062420caf35fa02cc0a4ae182fb74843d920 + languageName: node + linkType: hard + "json5@npm:^2.1.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -7874,6 +9488,27 @@ __metadata: languageName: node linkType: hard +"listr2@npm:^4.0.5": + version: 4.0.5 + resolution: "listr2@npm:4.0.5" + dependencies: + cli-truncate: "npm:^2.1.0" + colorette: "npm:^2.0.16" + log-update: "npm:^4.0.0" + p-map: "npm:^4.0.0" + rfdc: "npm:^1.3.0" + rxjs: "npm:^7.5.5" + through: "npm:^2.3.8" + wrap-ansi: "npm:^7.0.0" + peerDependencies: + enquirer: ">= 2.3.0 < 3" + peerDependenciesMeta: + enquirer: + optional: true + checksum: 10/9c591fdd4fd6b7e8b4feca60380be01d74c65a98857f6caff2418c609fb9f0016c2e1b65c0ef5b1f4ff015967be87e8642e7ac3ad7ce0aa3c1a0329b60128b3b + languageName: node + linkType: hard + "load-bmfont@npm:^1.4.1": version: 1.4.1 resolution: "load-bmfont@npm:1.4.1" @@ -7956,6 +9591,13 @@ __metadata: languageName: node linkType: hard +"lodash.sortby@npm:^4.7.0": + version: 4.7.0 + resolution: "lodash.sortby@npm:4.7.0" + checksum: 10/38df19ae28608af2c50ac342fc1f414508309d53e1d58ed9adfb2c3cd17c3af290058c0a0478028d932c5404df3d53349d19fa364ef6bed6145a6bc21320399e + languageName: node + linkType: hard + "lodash.uniq@npm:^4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" @@ -7963,14 +9605,14 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21": +"lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 languageName: node linkType: hard -"log-symbols@npm:^4.1.0": +"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -7980,6 +9622,18 @@ __metadata: languageName: node linkType: hard +"log-update@npm:^4.0.0": + version: 4.0.0 + resolution: "log-update@npm:4.0.0" + dependencies: + ansi-escapes: "npm:^4.3.0" + cli-cursor: "npm:^3.1.0" + slice-ansi: "npm:^4.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10/ae2f85bbabc1906034154fb7d4c4477c79b3e703d22d78adee8b3862fa913942772e7fa11713e3d96fb46de4e3cabefbf5d0a544344f03b58d3c4bff52aa9eb2 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -7991,6 +9645,15 @@ __metadata: languageName: node linkType: hard +"lower-case-first@npm:^2.0.2": + version: 2.0.2 + resolution: "lower-case-first@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/33e3da1098ddda219ce125d4ab7a78a944972c0ee8872e95b6ccc35df8ad405284ab233b0ba4d72315ad1a06fe2f0d418ee4cba9ec1ef1c386dea78899fc8958 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -8053,6 +9716,13 @@ __metadata: languageName: node linkType: hard +"map-cache@npm:^0.2.0": + version: 0.2.2 + resolution: "map-cache@npm:0.2.2" + checksum: 10/3067cea54285c43848bb4539f978a15dedc63c03022abeec6ef05c8cb6829f920f13b94bcaf04142fc6a088318e564c4785704072910d120d55dbc2e0c421969 + languageName: node + linkType: hard + "mdn-data@npm:2.0.28": version: 2.0.28 resolution: "mdn-data@npm:2.0.28" @@ -8129,6 +9799,18 @@ __metadata: languageName: node linkType: hard +"meros@npm:^1.2.1": + version: 1.3.0 + resolution: "meros@npm:1.3.0" + peerDependencies: + "@types/node": ">=13" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/1893d226866058a32161ab069294a1a16975c765416a2b05165dfafba07cd958ca12503e35c621ffe736c62d935ccb1ce60cb723e2a9e0b85e02bb3236722ef6 + languageName: node + linkType: hard + "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -8146,6 +9828,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.5": + version: 4.0.7 + resolution: "micromatch@npm:4.0.7" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10/a11ed1cb67dcbbe9a5fc02c4062cf8bb0157d73bf86956003af8dcfdf9b287f9e15ec0f6d6925ff6b8b5b496202335e497b01de4d95ef6cf06411bc5e5c474a0 + languageName: node + linkType: hard + "mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -8251,6 +9943,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^4.2.3": + version: 4.2.3 + resolution: "minimatch@npm:4.2.3" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/02bacb187448c6aeeed5a794a64799fb1d1dd0274694da97272a9d3b919c7dfba6987d2089f6465191450d36d767c47fd7958227610b919121ccaed7de7f8924 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -8434,6 +10135,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:0.0.8": + version: 0.0.8 + resolution: "mute-stream@npm:0.0.8" + checksum: 10/a2d2e79dde87e3424ffc8c334472c7f3d17b072137734ca46e6f221131f1b014201cc593b69a38062e974fb2394d3d1cb4349f80f012bbf8b8ac1b28033e515f + languageName: node + linkType: hard + "mute-stream@npm:1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" @@ -8538,7 +10246,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -8590,6 +10298,13 @@ __metadata: languageName: node linkType: hard +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: 10/b7afc2b65e56f7035b1a2eec57ae0fbdee7d742b1cdcd0f4387562b6527a011ab1cbe9f64cc8b3cca61e3297c9637c8bf61cec2e6b8d3a711d4b5267dfafbe02 + languageName: node + linkType: hard + "node-releases@npm:^2.0.14": version: 2.0.14 resolution: "node-releases@npm:2.0.14" @@ -8608,6 +10323,15 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^2.1.1": + version: 2.1.1 + resolution: "normalize-path@npm:2.1.1" + dependencies: + remove-trailing-separator: "npm:^1.0.1" + checksum: 10/7e9cbdcf7f5b8da7aa191fbfe33daf290cdcd8c038f422faf1b8a83c972bf7a6d94c5be34c4326cb00fb63bc0fd97d9fbcfaf2e5d6142332c2cd36d2e1b86cea + languageName: node + linkType: hard + "normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -8656,6 +10380,13 @@ __metadata: languageName: node linkType: hard +"nullthrows@npm:^1.1.1": + version: 1.1.1 + resolution: "nullthrows@npm:1.1.1" + checksum: 10/c7cf377a095535dc301d81cf7959d3784d090a609a2a4faa40b6121a0c1d7f70d3a3aa534a34ab852e8553b66848ec503c28f2c19efd617ed564dc07dfbb6d33 + languageName: node + linkType: hard + "nwsapi@npm:^2.2.10": version: 2.2.10 resolution: "nwsapi@npm:2.2.10" @@ -8663,7 +10394,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -8784,6 +10515,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:3.1.0, p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10/7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 + languageName: node + linkType: hard + "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -8793,15 +10533,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": - version: 3.1.0 - resolution: "p-limit@npm:3.1.0" - dependencies: - yocto-queue: "npm:^0.1.0" - checksum: 10/7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 - languageName: node - linkType: hard - "p-limit@npm:^4.0.0": version: 4.0.0 resolution: "p-limit@npm:4.0.0" @@ -8955,6 +10686,17 @@ __metadata: languageName: node linkType: hard +"parse-filepath@npm:^1.0.2": + version: 1.0.2 + resolution: "parse-filepath@npm:1.0.2" + dependencies: + is-absolute: "npm:^1.0.0" + map-cache: "npm:^0.2.0" + path-root: "npm:^0.1.1" + checksum: 10/6794c3f38d3921f0f7cc63fb1fb0c4d04cd463356ad389c8ce6726d3c50793b9005971f4138975a6d7025526058d5e65e9bfe634d0765e84c4e2571152665a69 + languageName: node + linkType: hard + "parse-headers@npm:^2.0.0": version: 2.0.5 resolution: "parse-headers@npm:2.0.5" @@ -9087,6 +10829,22 @@ __metadata: languageName: node linkType: hard +"path-root-regex@npm:^0.1.0": + version: 0.1.2 + resolution: "path-root-regex@npm:0.1.2" + checksum: 10/dcd75d1f8e93faabe35a58e875b0f636839b3658ff2ad8c289463c40bc1a844debe0dab73c3398ef9dc8f6ec6c319720aff390cf4633763ddcf3cf4b1bbf7e8b + languageName: node + linkType: hard + +"path-root@npm:^0.1.1": + version: 0.1.1 + resolution: "path-root@npm:0.1.1" + dependencies: + path-root-regex: "npm:^0.1.0" + checksum: 10/ff88aebfc1c59ace510cc06703d67692a11530989920427625e52b66a303ca9b3d4059b0b7d0b2a73248d1ad29bcb342b8b786ec00592f3101d38a45fd3b2e08 + languageName: node + linkType: hard + "path-scurry@npm:^1.10.2": version: 1.10.2 resolution: "path-scurry@npm:1.10.2" @@ -9156,6 +10914,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: 10/fa68166d1f56009fc02a34cdfd112b0dd3cf1ef57667ac57281f714065558c01828cdf4f18600ad6851cbe0093952ed0660b1e0156bddf2184b6aaf5817553a5 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -9727,6 +11492,15 @@ __metadata: languageName: node linkType: hard +"promise@npm:^7.1.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: "npm:~2.0.3" + checksum: 10/37dbe58ca7b0716cc881f0618128f1fd6ff9c46cdc529a269fd70004e567126a449a94e9428e2d19b53d06182d11b45d0c399828f103e06b2bb87643319bd2e7 + languageName: node + linkType: hard + "prop-types@npm:^15.6.2": version: 15.8.1 resolution: "prop-types@npm:15.8.1" @@ -9772,6 +11546,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^1.3.2": + version: 1.4.1 + resolution: "punycode@npm:1.4.1" + checksum: 10/af2700dde1a116791ff8301348ff344c47d6c224e875057237d1b5112035655fb07a6175cfdb8bf0e3a8cdfd2dc82b3a622e0aefd605566c0e949a6d0d1256a4 + languageName: node + linkType: hard + "punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -9779,6 +11560,22 @@ __metadata: languageName: node linkType: hard +"pvtsutils@npm:^1.3.2, pvtsutils@npm:^1.3.5": + version: 1.3.5 + resolution: "pvtsutils@npm:1.3.5" + dependencies: + tslib: "npm:^2.6.1" + checksum: 10/0a908edafe0e9db7ced6c0b86c897aaadd7389de73e54cc4aff1c8a887a623251bf43aedbc19f983336bffdd119959fa4e57eb544ca3ad5f6a30896a25c37fcc + languageName: node + linkType: hard + +"pvutils@npm:^1.1.3": + version: 1.1.3 + resolution: "pvutils@npm:1.1.3" + checksum: 10/e5201b8f78ece68eae414a938c844bc45fb3f0de298178eed1775a217eedfd897c4346e5e54f410bb4d7466e09ceb262e85f20fd64239b8bb2595f14c52fa95e + languageName: node + linkType: hard + "qs@npm:6.11.0": version: 6.11.0 resolution: "qs@npm:6.11.0" @@ -10179,6 +11976,38 @@ __metadata: languageName: node linkType: hard +"relay-runtime@npm:12.0.0": + version: 12.0.0 + resolution: "relay-runtime@npm:12.0.0" + dependencies: + "@babel/runtime": "npm:^7.0.0" + fbjs: "npm:^3.0.0" + invariant: "npm:^2.2.4" + checksum: 10/d6211e8206ea7273f88dccd5ea72abe6836c6f0bfe95a48ddf80c54e47a08edaf312bedecba98a0a0ba6abcd360cbacd6a2ddb4cef65f00170fb0f36cc324f5e + languageName: node + linkType: hard + +"remedial@npm:^1.0.7": + version: 1.0.8 + resolution: "remedial@npm:1.0.8" + checksum: 10/41e23a7d656fd696678e4f648e57ece5c9e13c097094e8ac6e173990a0665a24d8e50cbb39d458af3b0d58cfbd7811fc0840c4646d10ce3285fe5819b1c82375 + languageName: node + linkType: hard + +"remove-trailing-separator@npm:^1.0.1": + version: 1.1.0 + resolution: "remove-trailing-separator@npm:1.1.0" + checksum: 10/d3c20b5a2d987db13e1cca9385d56ecfa1641bae143b620835ac02a6b70ab88f68f117a0021838db826c57b31373d609d52e4f31aca75fc490c862732d595419 + languageName: node + linkType: hard + +"remove-trailing-spaces@npm:^1.0.6": + version: 1.0.8 + resolution: "remove-trailing-spaces@npm:1.0.8" + checksum: 10/81f615c5cd8dd6a5e3017dcc9af598965575d176d42ef99cfd7b894529991f464e629fd68aba089f5c6bebf5bb8070a5eee56f3b621aba55e8ef524d6a4d4f69 + languageName: node + linkType: hard + "renderkid@npm:^3.0.0": version: 3.0.0 resolution: "renderkid@npm:3.0.0" @@ -10192,6 +12021,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf + languageName: node + linkType: hard + "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -10199,6 +12035,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10/8604a570c06a69c9d939275becc33a65676529e1c3e5a9f42d58471674df79357872b96d70bb93a0380a62d60dc9031c98b1a9dad98c946ffdd61b7ac0c8cedd + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -10222,6 +12065,13 @@ __metadata: languageName: node linkType: hard +"resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10/be18a5e4d76dd711778664829841cde690971d02b6cbae277735a09c1c28f407b99ef6ef3cd585a1e6546d4097b28df40ed32c4a287b9699dcf6d7f208495e23 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -10229,13 +12079,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 10/be18a5e4d76dd711778664829841cde690971d02b6cbae277735a09c1c28f407b99ef6ef3cd585a1e6546d4097b28df40ed32c4a287b9699dcf6d7f208495e23 - languageName: node - linkType: hard - "resolve@npm:^1.14.2, resolve@npm:^1.20.0": version: 1.22.8 resolution: "resolve@npm:1.22.8" @@ -10293,6 +12136,13 @@ __metadata: languageName: node linkType: hard +"rfdc@npm:^1.3.0": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729 + languageName: node + linkType: hard + "rimraf@npm:^2.6.3": version: 2.7.1 resolution: "rimraf@npm:2.7.1" @@ -10347,6 +12197,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^2.4.0": + version: 2.4.1 + resolution: "run-async@npm:2.4.1" + checksum: 10/c79551224dafa26ecc281cb1efad3510c82c79116aaf681f8a931ce70fdf4ca880d58f97d3b930a38992c7aad7955a08e065b32ec194e1dd49d7790c874ece50 + languageName: node + linkType: hard + "run-async@npm:^3.0.0": version: 3.0.0 resolution: "run-async@npm:3.0.0" @@ -10363,7 +12220,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.2.0, rxjs@npm:^7.8.1": +"rxjs@npm:^7.2.0, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: @@ -10450,6 +12307,13 @@ __metadata: languageName: node linkType: hard +"scuid@npm:^1.1.0": + version: 1.1.0 + resolution: "scuid@npm:1.1.0" + checksum: 10/cd094ac3718b0070a222f9a499b280c698fdea10268cc163fa244421099544c1766dd893fdee0e2a8eba5d53ab9d0bcb11067bedff166665030fa6fda25a096b + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -10546,6 +12410,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -10560,6 +12431,13 @@ __metadata: languageName: node linkType: hard +"setimmediate@npm:^1.0.5": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: 10/76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485 + languageName: node + linkType: hard + "setprototypeof@npm:1.1.0": version: 1.1.0 resolution: "setprototypeof@npm:1.1.0" @@ -10668,7 +12546,7 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.8.1": +"shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d @@ -10700,6 +12578,13 @@ __metadata: languageName: node linkType: hard +"signedsource@npm:^1.0.0": + version: 1.0.0 + resolution: "signedsource@npm:1.0.0" + checksum: 10/64b2c8d7a48de9009cfd3aff62bb7c88abf3b8e0421f17ebb1d7f5ca9cc9c3ad10f5a1e3ae6cd804e4e6121c87b668202ae9057065f058ddfbf34ea65f63945d + languageName: node + linkType: hard + "simfile-parser@npm:^0.7.2": version: 0.7.2 resolution: "simfile-parser@npm:0.7.2" @@ -10757,6 +12642,28 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "slice-ansi@npm:3.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + astral-regex: "npm:^2.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + checksum: 10/5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 + languageName: node + linkType: hard + +"slice-ansi@npm:^4.0.0": + version: 4.0.0 + resolution: "slice-ansi@npm:4.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + astral-regex: "npm:^2.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + checksum: 10/4a82d7f085b0e1b070e004941ada3c40d3818563ac44766cca4ceadd2080427d337554f9f99a13aaeb3b4a94d9964d9466c807b3d7b7541d1ec37ee32d308756 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -10864,6 +12771,15 @@ __metadata: languageName: node linkType: hard +"sponge-case@npm:^1.0.1": + version: 1.0.1 + resolution: "sponge-case@npm:1.0.1" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/64f53d930f63c5a9e59d4cae487c1ffa87d25eab682833b01d572cc885e7e3fdbad4f03409a41f03ecb27f1f8959432253eb48332c7007c3388efddb24ba2792 + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -10911,7 +12827,21 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.3": +"streamsearch@npm:^1.1.0": + version: 1.1.0 + resolution: "streamsearch@npm:1.1.0" + checksum: 10/612c2b2a7dbcc859f74597112f80a42cbe4d448d03da790d5b7b39673c1197dd3789e91cd67210353e58857395d32c1e955a9041c4e6d5bae723436b3ed9ed14 + languageName: node + linkType: hard + +"string-env-interpolation@npm:^1.0.1": + version: 1.0.1 + resolution: "string-env-interpolation@npm:1.0.1" + checksum: 10/d126329587f635bee65300e4451e7352b9b67e03daeb62f006ca84244cac12a1f6e45176b018653ba0c3ec3b5d980f9ca59d2eeed99cf799501cdaa7f871dc6f + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -11079,6 +13009,15 @@ __metadata: languageName: node linkType: hard +"swap-case@npm:^2.0.2": + version: 2.0.2 + resolution: "swap-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/6e21c9e1b3cd5735eb2af679a99ec3efc78a14e3d4d5e3fd594e254b91cfd37185b3d1c6e41b22f53a2cdf5d1b963ce30c0fe8b78337e3fd43d0137084670a5f + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -11210,6 +13149,13 @@ __metadata: languageName: node linkType: hard +"through@npm:^2.3.6, through@npm:^2.3.8": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 + languageName: node + linkType: hard + "thunky@npm:^1.0.2": version: 1.1.0 resolution: "thunky@npm:1.1.0" @@ -11241,6 +13187,15 @@ __metadata: languageName: node linkType: hard +"title-case@npm:^3.0.3": + version: 3.0.3 + resolution: "title-case@npm:3.0.3" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/369fe90f650a66205c34ebef63a69c6d1fd411ae3aad23db0aae165ddb881af50e67c6ea6800d605bc2b9e0ab5f22dada58fe97a1a7e7f3131ee0ef176cc65ec + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -11329,7 +13284,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:~2.6.2": +"ts-log@npm:^2.2.3": + version: 2.2.5 + resolution: "ts-log@npm:2.2.5" + checksum: 10/b8fb444ae3b05ac8f709a1acee26dba014ed601e1fc36fa2bfcac5555032eb6c6ca9cd16b8da21832f1631785c3ad7de7177d8e7631c197a1aeca64f03a872a4 + languageName: node + linkType: hard + +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.1, tslib@npm:^2.6.2, tslib@npm:~2.6.0, tslib@npm:~2.6.2": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c @@ -11412,6 +13374,20 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.35": + version: 1.0.38 + resolution: "ua-parser-js@npm:1.0.38" + checksum: 10/f2345e9bd0f9c5f85bcaa434535fae88f4bb891538e568106f0225b2c2937fbfbeb5782bd22320d07b6b3d68b350b8861574c1d7af072ff9b2362fb72d326fd9 + languageName: node + linkType: hard + +"unc-path-regex@npm:^0.1.2": + version: 0.1.2 + resolution: "unc-path-regex@npm:0.1.2" + checksum: 10/a05fa2006bf4606051c10fc7968f08ce7b28fa646befafa282813aeb1ac1a56f65cb1b577ca7851af2726198d59475bb49b11776036257b843eaacee2860a4ec + languageName: node + linkType: hard + "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5" @@ -11505,6 +13481,15 @@ __metadata: languageName: node linkType: hard +"unixify@npm:^1.0.0": + version: 1.0.0 + resolution: "unixify@npm:1.0.0" + dependencies: + normalize-path: "npm:^2.1.1" + checksum: 10/3be30e48579fc6c7390bd59b4ab9e745fede0c164dfb7351cf710bd1dbef8484b1441186205af6bcb13b731c0c88caf9b33459f7bf8c89e79c046e656ae433f0 + languageName: node + linkType: hard + "unpipe@npm:1.0.0, unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" @@ -11526,6 +13511,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.0": + version: 1.1.0 + resolution: "update-browserslist-db@npm:1.1.0" + dependencies: + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/d70b9efeaf4601aadb1a4f6456a7a5d9118e0063d995866b8e0c5e0cf559482671dab6ce7b079f9536b06758a344fbd83f974b965211e1c6e8d1958540b0c24c + languageName: node + linkType: hard + "upper-case-first@npm:^2.0.2": version: 2.0.2 resolution: "upper-case-first@npm:2.0.2" @@ -11563,6 +13562,20 @@ __metadata: languageName: node linkType: hard +"urlpattern-polyfill@npm:^10.0.0": + version: 10.0.0 + resolution: "urlpattern-polyfill@npm:10.0.0" + checksum: 10/346819dbe718e929988298d02a988b8ddfa601d08daaa7e69b1148eab699c86c0f0f933d68d8c8cf913166fe64156ed28904e673200d18ef7e9ed6b58cea3fc7 + languageName: node + linkType: hard + +"urlpattern-polyfill@npm:^8.0.0": + version: 8.0.2 + resolution: "urlpattern-polyfill@npm:8.0.2" + checksum: 10/fd86b5c55473f3abbf9ed317b953c9cbb4fa6b3f75f681a1d982fe9c17bbc8d9bcf988f4cf3bda35e2e5875984086c97e177f97f076bb80dfa2beb85d1dd7b23 + languageName: node + linkType: hard + "use-sync-external-store@npm:1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" @@ -11627,6 +13640,13 @@ __metadata: languageName: node linkType: hard +"value-or-promise@npm:^1.0.11, value-or-promise@npm:^1.0.12": + version: 1.0.12 + resolution: "value-or-promise@npm:1.0.12" + checksum: 10/a4cc31fc9c3826b8a216ef2037b676904324c00c4acd903aaec2fe0c08516a189345261dd3cc822ec108532b2ea36b7c99bbdee1c3ddcb7f4b3d57d7e61b2064 + languageName: node + linkType: hard + "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -12094,13 +14114,26 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.0.3": +"web-streams-polyfill@npm:^3.0.3, web-streams-polyfill@npm:^3.2.1": version: 3.3.3 resolution: "web-streams-polyfill@npm:3.3.3" checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 languageName: node linkType: hard +"webcrypto-core@npm:^1.8.0": + version: 1.8.0 + resolution: "webcrypto-core@npm:1.8.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/json-schema": "npm:^1.1.12" + asn1js: "npm:^3.0.1" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + checksum: 10/c8403d32d99a9497d02a1ab4eee8a7691867d3f38817b1059e619a5603c22313d4a851ff3aa7ea59f1b3708d787c787b76b2484926cad2d5e01f584ae7c126b8 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -12329,6 +14362,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10/1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -12358,6 +14398,13 @@ __metadata: languageName: node linkType: hard +"wonka@npm:^6.3.2": + version: 6.3.4 + resolution: "wonka@npm:6.3.4" + checksum: 10/0f102630182828268b57b54102003449b97abbc2483392239baf856a2fca7b72ae9be67c208415124a3d26a320674ed64387e9bf07a8d0badedb5f607d2ccfdc + languageName: node + linkType: hard + "workerd@npm:1.20240610.1": version: 1.20240610.1 resolution: "workerd@npm:1.20240610.1" @@ -12384,7 +14431,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -12395,7 +14442,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.2.0": +"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" dependencies: @@ -12439,6 +14486,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.12.0, ws@npm:^8.17.1": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + "xhr@npm:^2.0.1": version: 2.6.0 resolution: "xhr@npm:2.6.0" @@ -12496,6 +14558,20 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10/392870b2a100bbc643bc035fe3a89cef5591b719c7bdc8721bcdb3d27ab39fa4870acdca67b0ee096e146d769f311d68eda6b8195a6d970f227795061923013f + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10/5f1b5f95e3775de4514edbb142398a2c37849ccfaf04a015be5d75521e9629d3be29bd4432d23c57f37e5b61ade592fb0197022e9993f81a06a5afbdcda9346d + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -12510,6 +14586,73 @@ __metadata: languageName: node linkType: hard +"yaml-ast-parser@npm:^0.0.43": + version: 0.0.43 + resolution: "yaml-ast-parser@npm:0.0.43" + checksum: 10/a54d00c8e0716a392c6e76eee965b3b4bba434494196490946e416fc47f20a1d89820461afacd9431edbb8209e28fce33bcff1fb42dd83f90e51fc31e80251c9 + languageName: node + linkType: hard + +"yaml@npm:^2.3.1": + version: 2.4.5 + resolution: "yaml@npm:2.4.5" + bin: + yaml: bin.mjs + checksum: 10/b09bf5a615a65276d433d76b8e34ad6b4c0320b85eb3f1a39da132c61ae6e2ff34eff4624e6458d96d49566c93cf43408ba5e568218293a8c6541a2006883f64 + languageName: node + linkType: hard + +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10/235bcbad5b7ca13e5abc54df61d42f230857c6f83223a38e4ed7b824681875b7f8b6ed52139d88a3ad007050f28dc0324b3c805deac7db22ae3b4815dae0e1bf + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10/9dc2c217ea3bf8d858041252d43e074f7166b53f3d010a8c711275e09cd3d62a002969a39858b92bbda2a6a63a585c7127014534a560b9c69ed2d923d113406e + languageName: node + linkType: hard + +"yargs@npm:^15.3.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10/bbcc82222996c0982905b668644ca363eebe6ffd6a572fbb52f0c0e8146661d8ce5af2a7df546968779bb03d1e4186f3ad3d55dfaadd1c4f0d5187c0e3a5ba16 + languageName: node + linkType: hard + +"yargs@npm:^17.0.0": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10/abb3e37678d6e38ea85485ed86ebe0d1e3464c640d7d9069805ea0da12f69d5a32df8e5625e370f9c96dd1c2dc088ab2d0a4dd32af18222ef3c4224a19471576 + languageName: node + linkType: hard + "yazl@npm:^2.5.1": version: 2.5.1 resolution: "yazl@npm:2.5.1" From f05677b88a9f959cd3ed09ccf16d6d8874b9020b Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 12:07:16 -0700 Subject: [PATCH 025/168] remove unused zustand utils --- src/zustand/contextual-zustand.ts | 74 ------------------- src/zustand/shared-zustand.ts | 118 ------------------------------ 2 files changed, 192 deletions(-) delete mode 100644 src/zustand/contextual-zustand.ts delete mode 100644 src/zustand/shared-zustand.ts diff --git a/src/zustand/contextual-zustand.ts b/src/zustand/contextual-zustand.ts deleted file mode 100644 index e879006e3..000000000 --- a/src/zustand/contextual-zustand.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - createContext, - ReactNode, - useContext, - useState, - createElement, - useEffect, -} from "react"; -import { StoreApi, createStore } from "zustand"; -import { useStoreWithEqualityFn } from "zustand/traditional"; - -export function createContextualStore< - StoreValue, - ProviderProps extends { children?: ReactNode }, ->( - /** returns initial store state for given props */ - creator: ( - p: ProviderProps, - set: StoreApi["setState"], - get: StoreApi["getState"], - ) => StoreValue, - /** returns a unique id for a given set of props */ - getUniqueId: (p: ProviderProps) => string, - globalProps: ProviderProps, -) { - const globalStore = createStore(creator.bind(undefined, globalProps)); - const context = createContext(globalStore); - const StoreIndex = new Map>(); - - const Provider = (props: ProviderProps) => { - const [localStore] = useState(() => - createStore(creator.bind(undefined, props)), - ); - useEffect(() => { - const thisId = getUniqueId(props); - StoreIndex.set(thisId, localStore); - return () => { - StoreIndex.delete(thisId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return createElement( - context.Provider, - { value: localStore }, - props.children, - ); - }; - - const useThisStore = () => useContext(context); - - function useContextValue(): StoreValue; - function useContextValue( - selector: (state: StoreValue) => Slice, - equalityFn?: (a: Slice, b: Slice) => boolean, - ): Slice; - function useContextValue( - selector?: (state: StoreValue) => Slice, - equalityFn?: (a: Slice, b: Slice) => boolean, - ) { - const store = useThisStore(); - return selector - ? // eslint-disable-next-line react-hooks/rules-of-hooks - useStoreWithEqualityFn(store, selector, equalityFn) - : // eslint-disable-next-line react-hooks/rules-of-hooks - useStoreWithEqualityFn(store); - } - - return { - Provider, - useContextValue, - StoreIndex, - useStore: useThisStore, - }; -} diff --git a/src/zustand/shared-zustand.ts b/src/zustand/shared-zustand.ts deleted file mode 100644 index 95ba95ac6..000000000 --- a/src/zustand/shared-zustand.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { DataConnection } from "peerjs"; -import type { StoreApi } from "zustand"; - -export interface SerializibleStore { - id: string; - serializeSyncFields(): ReducedState; -} - -type SyncMessages = InitSync | StateUpdate | { type: "other" }; - -interface InitSync { - type: "syncedStore.init"; - storeType: string; - storeId: string; - timestamp: number; - state: State; -} - -interface StateUpdate { - type: "syncedStore.stateUpdate"; - storeId: string; - timestamp: number; - state: State; -} - -export function acceptIncomingSyncedStores( - storeType: string, - peer: DataConnection, - handleNewStore: (initialState: SharedState) => void, -) { - const handlePeerMessage = (evt: SyncMessages) => { - if (evt.type !== "syncedStore.init") { - return; - } - if (evt.storeType !== storeType) { - return; - } - handleNewStore(evt.state); - }; - - peer.on("data", handlePeerMessage); - - return () => { - peer.off("data", handlePeerMessage); - }; -} - -export function initShareWithPeer( - storeType: string, - store: StoreApi>, - peer: DataConnection, -) { - const state = store.getState(); - sendMessage(peer, { - type: "syncedStore.init", - storeType, - storeId: state.id, - state: state.serializeSyncFields(), - timestamp: 0, - }); -} - -function sendMessage(peer: DataConnection, msg: SyncMessages) { - peer.send(msg); -} - -/** - * Based on https://github.com/Tom-Julux/shared-zustand - * @returns unsync function - */ -export function syncStoreWithPeer>( - store: StoreApi, - peer: DataConnection, -) { - const storeId = store.getState().id; - let externalUpdate = false; - let timestamp = 0; - - const handleStoreUpdate = (state: State) => { - if (!externalUpdate) { - timestamp = Date.now(); - sendMessage(peer, { - type: "syncedStore.stateUpdate", - storeId: state.id, - timestamp, - state: state.serializeSyncFields(), - }); - } - externalUpdate = false; - }; - - const handlePeerMessage = (evt: SyncMessages) => { - switch (evt.type) { - case "syncedStore.stateUpdate": - if (evt.storeId !== storeId) { - return; - } - break; - default: - return; - } - - if (evt.timestamp <= timestamp) { - return; - } - externalUpdate = true; - timestamp = evt.timestamp; - store.setState(evt.state); - }; - - const unsubscribeFromStore = store.subscribe(handleStoreUpdate); - peer.on("data", handlePeerMessage); - - return () => { - peer.off("data", handlePeerMessage); - unsubscribeFromStore(); - }; -} From 648e575cb2d493e6b3fe5c5787a340cc80398a4a Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 12:08:38 -0700 Subject: [PATCH 026/168] groundwork for entrants state from startgg --- graphql.config.ts | 2 +- src/startgg-gql/entrants.graphql | 23 ++++++++++ src/startgg-gql/generated/gql.ts | 9 +++- src/startgg-gql/generated/graphql.ts | 16 +++++-- src/startgg-gql/index.ts | 64 ++++++++++++++++++++++++++-- src/startgg-gql/queries.ts | 10 ----- src/startgg-gql/sets.graphql | 25 +++++++++++ src/state/entrants.slice.ts | 25 +++++++++++ src/state/root-reducer.ts | 2 + webpack.config.js | 2 +- 10 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 src/startgg-gql/entrants.graphql delete mode 100644 src/startgg-gql/queries.ts create mode 100644 src/startgg-gql/sets.graphql create mode 100644 src/state/entrants.slice.ts diff --git a/graphql.config.ts b/graphql.config.ts index 56375f723..c1648823e 100644 --- a/graphql.config.ts +++ b/graphql.config.ts @@ -10,5 +10,5 @@ export default { }, }, ], - documents: ["./src/startgg-gql/queries.ts"], + documents: ["./src/startgg-gql/*.graphql"], }; diff --git a/src/startgg-gql/entrants.graphql b/src/startgg-gql/entrants.graphql new file mode 100644 index 000000000..f2865c392 --- /dev/null +++ b/src/startgg-gql/entrants.graphql @@ -0,0 +1,23 @@ +query EventEntrants($eventSlug: String!, $pageNo: Int!) { + event(slug: $eventSlug) { + id + name + entrants(query: { page: $pageNo, perPage: 100 }) { + pageInfo { + totalPages + } + nodes { + id + name + # paginatedSets { + # nodes { + # id + # } + # pageInfo { + # totalPages + # } + # } + } + } + } +} diff --git a/src/startgg-gql/generated/gql.ts b/src/startgg-gql/generated/gql.ts index bf3542bfc..80ff6080b 100644 --- a/src/startgg-gql/generated/gql.ts +++ b/src/startgg-gql/generated/gql.ts @@ -13,7 +13,8 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - "\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n": types.EventEntrantsDocument, + "query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}": types.EventEntrantsDocument, + "query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}": types.EventSetsDocument, }; /** @@ -33,7 +34,11 @@ export function graphql(source: string): unknown; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n"): (typeof documents)["\n query EventEntrants($eventSlug: String) {\n event(slug: $eventSlug) {\n id\n name\n }\n }\n"]; +export function graphql(source: "query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}"): (typeof documents)["query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}"): (typeof documents)["query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/startgg-gql/generated/graphql.ts b/src/startgg-gql/generated/graphql.ts index e77ac7332..59cca4ff3 100644 --- a/src/startgg-gql/generated/graphql.ts +++ b/src/startgg-gql/generated/graphql.ts @@ -2403,11 +2403,21 @@ export type WaveUpsertInput = { }; export type EventEntrantsQueryVariables = Exact<{ - eventSlug?: InputMaybe; + eventSlug: Scalars['String']['input']; + pageNo: Scalars['Int']['input']; }>; -export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, name?: string | null } | null }; +export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, name?: string | null, entrants?: { __typename?: 'EntrantConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null } | null, nodes?: Array<{ __typename?: 'Entrant', id?: string | null, name?: string | null } | null> | null } | null } | null }; + +export type EventSetsQueryVariables = Exact<{ + eventSlug: Scalars['String']['input']; + pageNo: Scalars['Int']['input']; +}>; + + +export type EventSetsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, sets?: { __typename?: 'SetConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null, total?: number | null } | null, nodes?: Array<{ __typename?: 'Set', id?: string | null, fullRoundText?: string | null, identifier?: string | null, slots?: Array<{ __typename?: 'SetSlot', prereqType?: string | null, prereqId?: string | null, prereqPlacement?: number | null, entrant?: { __typename?: 'Entrant', id?: string | null, name?: string | null } | null } | null> | null } | null> | null } | null } | null }; -export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"entrants"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const EventSetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventSets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hideEmpty"},"value":{"kind":"BooleanValue","value":true}}]}},{"kind":"Argument","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"slots"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prereqType"}},{"kind":"Field","name":{"kind":"Name","value":"prereqId"}},{"kind":"Field","name":{"kind":"Name","value":"prereqPlacement"}},{"kind":"Field","name":{"kind":"Name","value":"entrant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index deb490bf5..1be3c15ed 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -1,4 +1,5 @@ -import { EventEntrantsDocument } from "./generated/graphql"; +import { Entrant } from "../state/entrants.slice"; +import { EventEntrantsDocument, EventSetsDocument } from "./generated/graphql"; import { Client, cacheExchange, fetchExchange } from "@urql/core"; const client = new Client({ @@ -11,6 +12,63 @@ const client = new Client({ exchanges: [cacheExchange, fetchExchange], }); -export function getEventEntrants(slug: string) { - return client.query(EventEntrantsDocument, { eventSlug: slug }); +export async function getEventEntrants(slug: string) { + let pageNo = 0; + + const ret: Entrant[] = []; + + let totalPages = 0; + do { + const results = await client.query(EventEntrantsDocument, { + eventSlug: slug, + pageNo, + }); + totalPages = results.data?.event?.entrants?.pageInfo?.totalPages || 0; + if (results.data?.event?.entrants?.nodes) { + for (const entrant of results.data.event.entrants.nodes) { + if (!entrant) continue; + ret.push({ + id: entrant.id!, + startggTag: entrant.name!, + }); + } + } + pageNo++; + } while (totalPages > pageNo + 1); + + return ret; +} + +export interface TournamentSet { + id: string; + roundText: string; + playerIds: string[]; +} + +export async function getEventSets(slug: string) { + let pageNo = 0; + + const ret: TournamentSet[] = []; + + let totalPages = 0; + do { + const results = await client.query(EventSetsDocument, { + eventSlug: slug, + pageNo, + }); + totalPages = results.data?.event?.sets?.pageInfo?.totalPages || 0; + if (results.data?.event?.sets?.nodes) { + for (const set of results.data.event.sets.nodes) { + if (!set) continue; + ret.push({ + id: set.id!, + roundText: set.fullRoundText!, + playerIds: set.slots!.map((slot) => slot!.entrant!.id!), + }); + } + } + pageNo++; + } while (totalPages > pageNo + 1); + + return ret; } diff --git a/src/startgg-gql/queries.ts b/src/startgg-gql/queries.ts deleted file mode 100644 index 9f6e66085..000000000 --- a/src/startgg-gql/queries.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from "@urql/core"; - -gql` - query EventEntrants($eventSlug: String) { - event(slug: $eventSlug) { - id - name - } - } -`; diff --git a/src/startgg-gql/sets.graphql b/src/startgg-gql/sets.graphql new file mode 100644 index 000000000..386e25f6c --- /dev/null +++ b/src/startgg-gql/sets.graphql @@ -0,0 +1,25 @@ +query EventSets($eventSlug: String!, $pageNo: Int!) { + event(slug: $eventSlug) { + id + sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) { + pageInfo { + totalPages + total + } + nodes { + id + fullRoundText + identifier + slots { + prereqType + prereqId + prereqPlacement + entrant { + id + name + } + } + } + } + } +} diff --git a/src/state/entrants.slice.ts b/src/state/entrants.slice.ts new file mode 100644 index 000000000..399275ba3 --- /dev/null +++ b/src/state/entrants.slice.ts @@ -0,0 +1,25 @@ +import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; + +export interface Entrant { + id: string; + ingameName?: string; + startggTag: string; +} + +const entrantsAdapter = createEntityAdapter(); +const selectors = entrantsAdapter.getSelectors(); + +export const entrantsSlice = createSlice({ + name: "entrants", + initialState: entrantsAdapter.getInitialState(), + reducers: { + removeMany: entrantsAdapter.removeMany, + upsertMany: entrantsAdapter.upsertMany, + upsertOne: entrantsAdapter.updateOne, + }, + selectors: { + selectAll: selectors.selectAll, + selectById: selectors.selectById, + selectIds: selectors.selectIds, + }, +}); diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index d0ffe07a6..faa547958 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -4,12 +4,14 @@ import { drawingsSlice } from "./drawings.slice"; import { gameDataSlice } from "./game-data.slice"; import { receivePartyState } from "./central"; import { eventSlice } from "./event.slice"; +import { entrantsSlice } from "./entrants.slice"; const combinedReducer = combineSlices( drawingsSlice, configSlice, gameDataSlice, eventSlice, + entrantsSlice, ); export const reducer: typeof combinedReducer = (state, action) => { diff --git a/webpack.config.js b/webpack.config.js index 0c43b5464..4c68a9f96 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,4 @@ -require("dotenv").setup(); +require("dotenv/config"); const fs = require("fs"); const { resolve, basename } = require("path"); From a5cd4754f6940e8314c58fe1979355eb54012aa0 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 12:28:36 -0700 Subject: [PATCH 027/168] move participatns tab from drawer to main view --- src/app.tsx | 4 ++-- src/controls/controls-drawer.tsx | 31 +------------------------------ src/main-view.css | 16 ++++++++++++++++ src/main-view.tsx | 28 ++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 src/main-view.css create mode 100644 src/main-view.tsx diff --git a/src/app.tsx b/src/app.tsx index aed79b5bb..28ed1ec3b 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -8,7 +8,6 @@ import { FocusStyleManager } from "@blueprintjs/core"; FocusStyleManager.onlyShowFocusOnTabs(); -import { DrawingList } from "./drawing-list"; import { UpdateManager } from "./update-manager"; import { IntlProvider } from "./intl-provider"; import { Header } from "./header"; @@ -25,6 +24,7 @@ import { useParams, } from "react-router-dom"; import { CabManagement } from "./cab-management"; +import { MainView } from "./main-view"; const router = createBrowserRouter([ { @@ -115,7 +115,7 @@ function AppForRoom() { }} > - + diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index 6ec0bf63b..03f2ab727 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -3,18 +3,13 @@ import { ButtonGroup, Card, Checkbox, - Classes, Collapse, Divider, FormGroup, HTMLSelect, NumericInput, - Tab, - Tabs, } from "@blueprintjs/core"; import { - Settings, - People, CaretDown, CaretRight, Plus, @@ -31,7 +26,6 @@ import { useIsNarrow } from "../hooks/useMediaQuery"; import { GameData } from "../models/SongData"; import { WeightsControls } from "./controls-weights"; import styles from "./controls.css"; -import { PlayerNamesControls } from "./player-names"; import { getAvailableLevels } from "../game-data-utils"; import { ShowChartsToggle } from "./show-charts-toggle"; import { Fraction } from "../utils/fraction"; @@ -80,32 +74,9 @@ function getDiffsAndRangeForNewStyle( } export default function ControlsDrawer() { - const { t } = useIntl(); return (
- - } - panel={} - > - {t("controls.tabs.general")} - - {/* } />} - panel={} - > - {t("controls.tabs.networking")} - */} - } - panel={} - > - {t("controls.tabs.players")} - - +
); } diff --git a/src/main-view.css b/src/main-view.css new file mode 100644 index 000000000..dc46a6fcc --- /dev/null +++ b/src/main-view.css @@ -0,0 +1,16 @@ +.mainView { + flex: 1 1 0px; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.mainView > div:first-child { + flex: 0 0 auto; +} + +.mainView > div:not(:first-child) { + flex: 1 1 auto; + overflow: hidden; +} diff --git a/src/main-view.tsx b/src/main-view.tsx new file mode 100644 index 000000000..51b7a5c72 --- /dev/null +++ b/src/main-view.tsx @@ -0,0 +1,28 @@ +import { Tabs, Tab } from "@blueprintjs/core"; +import { PlayerNamesControls } from "./controls/player-names"; +import { DrawingList } from "./drawing-list"; +import { atom, useAtom } from "jotai"; +import styles from "./main-view.css"; + +export type MainTabId = "drawings" | "players"; +export const mainTabAtom = atom("drawings"); + +export function MainView() { + const [currentTab, setCurrentTab] = useAtom(mainTabAtom); + return ( + setCurrentTab(newTabId)} + > + }> + Drawings + + }> + Participants + + + ); +} From 9dea64df835608a74af3a9e30230faff8f840d90 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 14:16:12 -0700 Subject: [PATCH 028/168] first working name imports --- src/controls/player-names.tsx | 156 +++++++++++++++++----------------- src/main-view.css | 4 +- src/startgg-gql/index.ts | 24 +++--- 3 files changed, 94 insertions(+), 90 deletions(-) diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 359a16735..2ff49a422 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -2,62 +2,100 @@ import { Checkbox, Classes, FormGroup, + Label, + Text, NumericInput, - TagInput, } from "@blueprintjs/core"; -import { ReactNode } from "react"; +import React, { useCallback } from "react"; import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useIntl } from "../hooks/useIntl"; -import { DiagramTree, Person } from "@blueprintjs/icons"; -import { useAtom } from "jotai"; +import { atom, useAtom } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; +import { useAppDispatch, useAppState } from "../state/store"; +import { entrantsSlice, Entrant } from "../state/entrants.slice"; +import { getEventEntrants } from "../startgg-gql"; export function PlayerNamesControls() { const { t } = useIntl(); - const playerNames = useConfigState((s) => s.playerNames); - const updateConfig = useUpdateConfig(); - - function addPlayers(names: string[]) { - updateConfig((prev) => { - const next = prev.playerNames.slice(); - for (const name of names) { - if (!next.includes(name)) { - next.push(name); - } - } - if (next.length !== prev.playerNames.length) { - return { playerNames: next }; - } - return {}; - }); - } - function removePlayer(name: ReactNode, index: number) { - updateConfig((prev) => { - const next = prev.playerNames.slice(); - next.splice(index, 1); - return { playerNames: next }; - }); - } + const entrants = useAppState(entrantsSlice.selectors.selectAll); return ( <> - - } - onAdd={addPlayers} - onRemove={removePlayer} - /> - - + {entrants.length ? ( + + {entrants.map((e) => ( + + ))} + + ) : ( + + )} ); } +const startggKeyAtom = atom(process.env.STARTGG_TOKEN as string); +const startggEventSlug = atom(null); + +function StartggEntrantImport() { + const [apiKey, setApiKey] = useAtom(startggKeyAtom); + const [eventSlug, setEventSlug] = useAtom(startggEventSlug); + const dispatch = useAppDispatch(); + const importEntrants = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + if (!apiKey || !eventSlug) { + return; + } + const entrants = await getEventEntrants(apiKey, eventSlug); + dispatch(entrantsSlice.actions.upsertMany(entrants)); + }, + [dispatch, apiKey, eventSlug], + ); + return ( +
+ No players added yet, import from start.gg + + + +
+ ); +} + +function inferShortname(name: string) { + const namePieces = name.split(" | "); + return namePieces.length > 1 ? namePieces[namePieces.length - 1] : undefined; +} + +function EntrantNameForm(props: { entrant: Entrant }) { + return ( + + ); +} + function ShowLabelsToggle() { const [enabled, updateShowLabels] = useAtom(showPlayerAndRoundLabels); const { t } = useIntl(); @@ -90,43 +128,3 @@ function PlayersPerDraw() { ); } - -function TournamentLabelEditor() { - const { t } = useIntl(); - const tournamentRounds = useConfigState((s) => s.tournamentRounds); - const updateConfig = useUpdateConfig(); - - function addLabels(names: string[]) { - updateConfig((prev) => { - const next = prev.tournamentRounds.slice(); - for (const name of names) { - if (!next.includes(name)) { - next.push(name); - } - } - if (next.length !== prev.tournamentRounds.length) { - return { tournamentRounds: next }; - } - return {}; - }); - } - function removeLabel(name: ReactNode, index: number) { - updateConfig((prev) => { - const next = prev.tournamentRounds.slice(); - next.splice(index, 1); - return { tournamentRounds: next }; - }); - } - return ( - - } - onAdd={addLabels} - onRemove={removeLabel} - /> - - ); -} diff --git a/src/main-view.css b/src/main-view.css index dc46a6fcc..e5399647f 100644 --- a/src/main-view.css +++ b/src/main-view.css @@ -6,11 +6,13 @@ overflow: hidden; } +/* the tabs themselves */ .mainView > div:first-child { flex: 0 0 auto; } +/* all the full page tab contents */ .mainView > div:not(:first-child) { flex: 1 1 auto; - overflow: hidden; + overflow-y: auto; } diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 1be3c15ed..23dc4ea67 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -2,17 +2,20 @@ import { Entrant } from "../state/entrants.slice"; import { EventEntrantsDocument, EventSetsDocument } from "./generated/graphql"; import { Client, cacheExchange, fetchExchange } from "@urql/core"; -const client = new Client({ - url: "https://api.start.gg/gql/alpha", - fetchOptions: { - headers: { - Authorization: `Bearer ${process.env.STARTGG_TOKEN}`, +function getClient(token: string) { + return new Client({ + url: "https://api.start.gg/gql/alpha", + fetchOptions: { + headers: { + Authorization: `Bearer ${token}`, + }, }, - }, - exchanges: [cacheExchange, fetchExchange], -}); + exchanges: [cacheExchange, fetchExchange], + }); +} -export async function getEventEntrants(slug: string) { +export async function getEventEntrants(token: string, slug: string) { + const client = getClient(token); let pageNo = 0; const ret: Entrant[] = []; @@ -45,7 +48,8 @@ export interface TournamentSet { playerIds: string[]; } -export async function getEventSets(slug: string) { +export async function getEventSets(token: string, slug: string) { + const client = getClient(token); let pageNo = 0; const ret: TournamentSet[] = []; From 453681c0c3b118740c603b79e032f86f2d629647 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 17:04:47 -0700 Subject: [PATCH 029/168] prototype of set imports --- src/cab-management.tsx | 4 ++ src/controls/player-names.tsx | 6 ++- src/main-view.tsx | 6 ++- src/matches.tsx | 72 +++++++++++++++++++++++++++++++++++ src/startgg-gql/index.ts | 10 ++--- src/state/entrants.slice.ts | 4 +- src/state/matches.slice.ts | 25 ++++++++++++ src/state/root-reducer.ts | 2 + 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 src/matches.tsx create mode 100644 src/state/matches.slice.ts diff --git a/src/cab-management.tsx b/src/cab-management.tsx index c50156332..48ff27023 100644 --- a/src/cab-management.tsx +++ b/src/cab-management.tsx @@ -22,6 +22,8 @@ import { } from "@blueprintjs/icons"; import { detectedLanguage } from "./utils"; import { copyPlainTextToClipboard } from "./utils/share"; +import { useSetAtom } from "jotai"; +import { mainTabAtom } from "./main-view"; export function CabManagement() { const cabs = Object.values(useAppState((s) => s.event.cabs)); @@ -155,6 +157,7 @@ function CurrentMatch(props: { cab: CabInfo }) { if (!props.cab.activeMatch) return null; return s.drawings.entities[props.cab.activeMatch] || null; }); + const setMainTab = useSetAtom(mainTabAtom); const scrollToDrawing = useCallback(() => { if (!drawing) { @@ -170,6 +173,7 @@ function CurrentMatch(props: { cab: CabInfo }) { if (priorFocus) { delete priorFocus.dataset.focused; } + setMainTab("drawings"); el.scrollIntoView({ behavior: "smooth" }); el.dataset.focused = ""; }, [drawing]); diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 2ff49a422..6ad8d709b 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -36,8 +36,10 @@ export function PlayerNamesControls() { ); } -const startggKeyAtom = atom(process.env.STARTGG_TOKEN as string); -const startggEventSlug = atom(null); +export const startggKeyAtom = atom( + process.env.STARTGG_TOKEN as string, +); +export const startggEventSlug = atom(null); function StartggEntrantImport() { const [apiKey, setApiKey] = useAtom(startggKeyAtom); diff --git a/src/main-view.tsx b/src/main-view.tsx index 51b7a5c72..0183dc83f 100644 --- a/src/main-view.tsx +++ b/src/main-view.tsx @@ -3,8 +3,9 @@ import { PlayerNamesControls } from "./controls/player-names"; import { DrawingList } from "./drawing-list"; import { atom, useAtom } from "jotai"; import styles from "./main-view.css"; +import { MatchListAndSettings } from "./matches"; -export type MainTabId = "drawings" | "players"; +export type MainTabId = "drawings" | "players" | "sets"; export const mainTabAtom = atom("drawings"); export function MainView() { @@ -23,6 +24,9 @@ export function MainView() { }> Participants + }> + Sets + ); } diff --git a/src/matches.tsx b/src/matches.tsx new file mode 100644 index 000000000..2cc273010 --- /dev/null +++ b/src/matches.tsx @@ -0,0 +1,72 @@ +import { Classes, Label, Text } from "@blueprintjs/core"; +import { useAtom } from "jotai"; +import { useCallback } from "react"; +import { startggKeyAtom, startggEventSlug } from "./controls/player-names"; +import { getEventSets } from "./startgg-gql"; +import { useAppDispatch, useAppState } from "./state/store"; +import { setsSlice, TournamentSet } from "./state/matches.slice"; +import { entrantsSlice } from "./state/entrants.slice"; + +export function MatchListAndSettings() { + const sets = useAppState(setsSlice.selectors.selectAll); + return sets.length ? ( + <> + {sets.map((set) => ( + + ))} + + ) : ( + + ); +} + +function TournamentSetPreview(props: { set: TournamentSet }) { + const players = useAppState((s) => + entrantsSlice.selectors.selectFromIds(s, props.set.playerIds), + ); + return ( + + {props.set.roundText} - {players[0]?.startggTag} vs{" "} + {players[1]?.startggTag || ???} + + ); +} + +function StartggSetImport() { + const [apiKey, setApiKey] = useAtom(startggKeyAtom); + const [eventSlug, setEventSlug] = useAtom(startggEventSlug); + const dispatch = useAppDispatch(); + const importEntrants = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + if (!apiKey || !eventSlug) { + return; + } + const sets = await getEventSets(apiKey, eventSlug); + dispatch(setsSlice.actions.upsertMany(sets)); + }, + [dispatch, apiKey, eventSlug], + ); + return ( +
+ No sets added yet, import from start.gg + + + +
+ ); +} diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 23dc4ea67..2c3632081 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -1,4 +1,5 @@ import { Entrant } from "../state/entrants.slice"; +import { TournamentSet } from "../state/matches.slice"; import { EventEntrantsDocument, EventSetsDocument } from "./generated/graphql"; import { Client, cacheExchange, fetchExchange } from "@urql/core"; @@ -42,12 +43,6 @@ export async function getEventEntrants(token: string, slug: string) { return ret; } -export interface TournamentSet { - id: string; - roundText: string; - playerIds: string[]; -} - export async function getEventSets(token: string, slug: string) { const client = getClient(token); let pageNo = 0; @@ -64,10 +59,11 @@ export async function getEventSets(token: string, slug: string) { if (results.data?.event?.sets?.nodes) { for (const set of results.data.event.sets.nodes) { if (!set) continue; + const players = set.slots!.map((slot) => slot!.entrant?.id); ret.push({ id: set.id!, roundText: set.fullRoundText!, - playerIds: set.slots!.map((slot) => slot!.entrant!.id!), + playerIds: players.filter((pid): pid is string => !!pid), }); } } diff --git a/src/state/entrants.slice.ts b/src/state/entrants.slice.ts index 399275ba3..e4fc69700 100644 --- a/src/state/entrants.slice.ts +++ b/src/state/entrants.slice.ts @@ -1,5 +1,4 @@ import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; - export interface Entrant { id: string; ingameName?: string; @@ -20,6 +19,9 @@ export const entrantsSlice = createSlice({ selectors: { selectAll: selectors.selectAll, selectById: selectors.selectById, + selectFromIds(state, entrantIds: string[]) { + return entrantIds.map((id) => selectors.selectById(state, id)); + }, selectIds: selectors.selectIds, }, }); diff --git a/src/state/matches.slice.ts b/src/state/matches.slice.ts new file mode 100644 index 000000000..619220a08 --- /dev/null +++ b/src/state/matches.slice.ts @@ -0,0 +1,25 @@ +import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; + +export interface TournamentSet { + id: string; + roundText: string; + playerIds: string[]; +} + +const setsAdapter = createEntityAdapter(); +const selectors = setsAdapter.getSelectors(); + +export const setsSlice = createSlice({ + name: "sets", + initialState: setsAdapter.getInitialState(), + reducers: { + removeMany: setsAdapter.removeMany, + upsertMany: setsAdapter.upsertMany, + upsertOne: setsAdapter.updateOne, + }, + selectors: { + selectAll: selectors.selectAll, + selectById: selectors.selectById, + selectIds: selectors.selectIds, + }, +}); diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index faa547958..65e9f37fd 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -5,6 +5,7 @@ import { gameDataSlice } from "./game-data.slice"; import { receivePartyState } from "./central"; import { eventSlice } from "./event.slice"; import { entrantsSlice } from "./entrants.slice"; +import { setsSlice } from "./matches.slice"; const combinedReducer = combineSlices( drawingsSlice, @@ -12,6 +13,7 @@ const combinedReducer = combineSlices( gameDataSlice, eventSlice, entrantsSlice, + setsSlice, ); export const reducer: typeof combinedReducer = (state, action) => { From 14c65c19bc18be4af0527c480bc655283d9bba06 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 18 Jul 2024 22:07:59 -0700 Subject: [PATCH 030/168] preliminary handling of prereqs --- src/controls/player-names.tsx | 4 +++- src/matches.tsx | 24 +++++++++++++++++------- src/startgg-gql/index.ts | 7 +++++-- src/state/matches.slice.ts | 15 ++++++++++++++- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 6ad8d709b..751ddfba6 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -39,7 +39,9 @@ export function PlayerNamesControls() { export const startggKeyAtom = atom( process.env.STARTGG_TOKEN as string, ); -export const startggEventSlug = atom(null); +export const startggEventSlug = atom( + "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", +); function StartggEntrantImport() { const [apiKey, setApiKey] = useAtom(startggKeyAtom); diff --git a/src/matches.tsx b/src/matches.tsx index 2cc273010..50d492181 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -3,8 +3,8 @@ import { useAtom } from "jotai"; import { useCallback } from "react"; import { startggKeyAtom, startggEventSlug } from "./controls/player-names"; import { getEventSets } from "./startgg-gql"; -import { useAppDispatch, useAppState } from "./state/store"; -import { setsSlice, TournamentSet } from "./state/matches.slice"; +import { AppState, useAppDispatch, useAppState } from "./state/store"; +import { setsSlice, Slot, TournamentSet } from "./state/matches.slice"; import { entrantsSlice } from "./state/entrants.slice"; export function MatchListAndSettings() { @@ -21,13 +21,23 @@ export function MatchListAndSettings() { } function TournamentSetPreview(props: { set: TournamentSet }) { - const players = useAppState((s) => - entrantsSlice.selectors.selectFromIds(s, props.set.playerIds), - ); + const [slot1, slot2] = props.set.slots; + const getPlayerForSlot = (slot: Slot) => (s: AppState) => { + if (slot.type === "player") + return entrantsSlice.selectors.selectById(s, slot.playerId); + const prereqSet = setsSlice.selectors.selectById(s, slot.setId); + if (prereqSet.winningPlayerId) { + return entrantsSlice.selectors.selectById(s, prereqSet.winningPlayerId); + } + return null; + }; + const player1 = useAppState(getPlayerForSlot(slot1)); + const player2 = useAppState(getPlayerForSlot(slot2)); return ( - {props.set.roundText} - {players[0]?.startggTag} vs{" "} - {players[1]?.startggTag || ???} + {props.set.roundText} -{" "} + {player1?.startggTag || TBD} vs{" "} + {player2?.startggTag || TBD} ); } diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 2c3632081..444a8de10 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -59,11 +59,14 @@ export async function getEventSets(token: string, slug: string) { if (results.data?.event?.sets?.nodes) { for (const set of results.data.event.sets.nodes) { if (!set) continue; - const players = set.slots!.map((slot) => slot!.entrant?.id); ret.push({ id: set.id!, roundText: set.fullRoundText!, - playerIds: players.filter((pid): pid is string => !!pid), + slots: set.slots!.map((slot) => + slot!.entrant?.id + ? { type: "player", playerId: slot!.entrant.id } + : { type: "setprereq", setId: slot!.prereqId! }, + ), }); } } diff --git a/src/state/matches.slice.ts b/src/state/matches.slice.ts index 619220a08..d372b5098 100644 --- a/src/state/matches.slice.ts +++ b/src/state/matches.slice.ts @@ -1,9 +1,22 @@ import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; +interface SetPlayer { + type: "player"; + playerId: string; +} + +interface SetPrereq { + type: "setprereq"; + setId: string; +} + +export type Slot = SetPlayer | SetPrereq; + export interface TournamentSet { id: string; roundText: string; - playerIds: string[]; + slots: Slot[]; + winningPlayerId?: string; } const setsAdapter = createEntityAdapter(); From 905134dc9e1b3b0ceaf2fd585296c1ff2a075c3a Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 26 Jul 2024 23:34:03 -0700 Subject: [PATCH 031/168] cleaner root handler --- src/app.tsx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 28ed1ec3b..367ef0819 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -22,21 +22,27 @@ import { Outlet, RouterProvider, useParams, + Link, } from "react-router-dom"; import { CabManagement } from "./cab-management"; import { MainView } from "./main-view"; +import { nanoid } from "nanoid"; const router = createBrowserRouter([ { path: "/", - element: ( - <> -

- You need to pick an event first. How about this one:{" "} - Default Event -

- - ), + Component: () => { + return ( +
+

DDR Tools Event Mode

+

Alpha Preview

+

+ You need to pick an event first. Would you like to:{" "} + Create New Event? +

+
+ ); + }, }, { path: "e/:roomName", From e122c3b0217f0b269a3703d82f0b2f62369ccf42 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 26 Jul 2024 23:52:55 -0700 Subject: [PATCH 032/168] temporary rollback for player name management --- src/controls/player-names.tsx | 61 +++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 751ddfba6..1e1fe4be3 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -5,6 +5,7 @@ import { Label, Text, NumericInput, + TagInput, } from "@blueprintjs/core"; import React, { useCallback } from "react"; import { useConfigState, useUpdateConfig } from "../state/hooks"; @@ -14,24 +15,49 @@ import { showPlayerAndRoundLabels } from "../config-state"; import { useAppDispatch, useAppState } from "../state/store"; import { entrantsSlice, Entrant } from "../state/entrants.slice"; import { getEventEntrants } from "../startgg-gql"; +import { Person } from "@blueprintjs/icons"; export function PlayerNamesControls() { + const updateConfig = useUpdateConfig(); + const playerNames = useConfigState((s) => s.playerNames); const { t } = useIntl(); - const entrants = useAppState(entrantsSlice.selectors.selectAll); + + function addPlayers(names: string[]) { + updateConfig((prev) => { + const next = prev.playerNames.slice(); + for (const name of names) { + if (!next.includes(name)) { + next.push(name); + } + } + if (next.length !== prev.playerNames.length) { + return { playerNames: next }; + } + return {}; + }); + } + function removePlayer(name: React.ReactNode, index: number) { + updateConfig((prev) => { + const next = prev.playerNames.slice(); + next.splice(index, 1); + return { playerNames: next }; + }); + } return ( <> - {entrants.length ? ( - - {entrants.map((e) => ( - - ))} - - ) : ( - - )} + + } + onAdd={addPlayers} + onRemove={removePlayer} + /> + ); } @@ -43,6 +69,21 @@ export const startggEventSlug = atom( "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", ); +export function StartggEntrantManager() { + const { t } = useIntl(); + const entrants = useAppState(entrantsSlice.selectors.selectAll); + if (!entrants.length) { + return ; + } + return ( + + {entrants.map((e) => ( + + ))} + + ); +} + function StartggEntrantImport() { const [apiKey, setApiKey] = useAtom(startggKeyAtom); const [eventSlug, setEventSlug] = useAtom(startggEventSlug); From 1a0e53fd784c6b07214870c622ca65714bb6f411 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 27 Jul 2024 11:43:06 -0700 Subject: [PATCH 033/168] fix reorder by pick/ban --- src/song-card/song-card.tsx | 5 +++- src/state/drawings.slice.ts | 46 +++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index 3170bc892..af38c1099 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -56,6 +56,7 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { player: number, pick?: EligibleChart, ) => { + const reorder = store.getState().config.orderByAction; let action; if (type === "pocket") { if (pick) { @@ -65,6 +66,7 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { type, player, pick, + reorder, }); } } else { @@ -73,13 +75,14 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { chartId, type, player, + reorder, }); } if (action) { dispatch(action); } }, - [drawingId, chartId, dispatch], + [drawingId, chartId, dispatch, store], ); return useMemo( diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 48d6e4924..f7d86ffbc 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -62,6 +62,15 @@ type ActionOnSingleDrawing = PayloadAction; type ActionOnSingleChart = PayloadAction< { drawingId: string; chartId: string } & extra >; +// eslint-disable-next-line @typescript-eslint/ban-types +type PlayerActionOnChartPayload = PayloadAction< + { + drawingId: string; + chartId: string; + player: number; + reorder: boolean; + } & extra +>; export const drawingsSlice = createSlice({ name: "drawings", @@ -125,22 +134,30 @@ export const drawingsSlice = createSlice({ }, banProtectReplace( state, - action: ActionOnSingleChart< - | { type: "ban" | "protect"; player: number } - | { type: "pocket"; player: number; pick: EligibleChart } + action: PlayerActionOnChartPayload< + { type: "ban" | "protect" } | { type: "pocket"; pick: EligibleChart } >, ) { - const { chartId, drawingId, player } = action.payload; + const { chartId, drawingId, player, reorder } = action.payload; const drawing = state.entities[drawingId]; if (!drawing) { return; } const playerAction: PlayerActionOnChart = { chartId, player }; if (action.payload.type === "ban") { + if (reorder) { + moveChartInArray(drawing, chartId, "end"); + } drawing.bans[chartId] = playerAction; } else if (action.payload.type === "protect") { + if (reorder) { + moveChartInArray(drawing, chartId, "start"); + } drawing.protects[chartId] = playerAction; } else if (action.payload.type === "pocket") { + if (reorder) { + moveChartInArray(drawing, chartId, "start"); + } drawing.pocketPicks[chartId] = { chartId, player, @@ -221,3 +238,24 @@ export function createRedrawChart( changes: chart, }); } + +function moveChartInArray( + drawing: Drawing, + chartId: string, + pos: "start" | "end", +) { + const targetChart = drawing.charts.find((c) => c.id === chartId); + if (!targetChart) { + return; + } + const chartsWithoutTarget = drawing.charts.filter((c) => c.id !== chartId); + if (pos === "start") { + const insertIdx = + Object.keys(drawing.protects).length + + Object.keys(drawing.pocketPicks).length; + chartsWithoutTarget.splice(insertIdx, 0, targetChart); + } else { + chartsWithoutTarget.push(targetChart); + } + drawing.charts = chartsWithoutTarget; +} From 16e0e39fe9da994b93cb34385747a6c9b26ae5bb Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 27 Jul 2024 11:49:56 -0700 Subject: [PATCH 034/168] fix jacket images in search results --- src/song-card/song-card.tsx | 4 ++-- src/song-jacket.tsx | 3 ++- src/utils/jackets.ts | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/utils/jackets.ts diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index af38c1099..c14f8f07f 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -18,6 +18,7 @@ import styles from "./song-card.css"; import { ChartLevel } from "./chart-level"; import { useAppDispatch, useAppStore } from "../state/store"; import { createRedrawChart, drawingsSlice } from "../state/drawings.slice"; +import { getJacketUrl } from "../utils/jackets"; const isJapanese = detectedLanguage === "ja"; @@ -155,9 +156,8 @@ export function SongCard(props: Props) { let jacketBg = {}; if (jacket) { - const prefix = jacket.startsWith("blob:") ? "" : "/jackets/"; jacketBg = { - backgroundImage: `url("${prefix}${jacket}")`, + backgroundImage: `url("${getJacketUrl(jacket)}")`, }; } diff --git a/src/song-jacket.tsx b/src/song-jacket.tsx index 0df854f5d..8cd171d59 100644 --- a/src/song-jacket.tsx +++ b/src/song-jacket.tsx @@ -1,6 +1,7 @@ import { Song } from "./models/SongData"; import { Icon } from "@blueprintjs/core"; import { Music } from "@blueprintjs/icons"; +import { getJacketUrl } from "./utils/jackets"; interface Props { song: Song; @@ -12,7 +13,7 @@ export function SongJacket(props: Props) { if (props.song.jacket) { return ( diff --git a/src/utils/jackets.ts b/src/utils/jackets.ts new file mode 100644 index 000000000..c5237c06a --- /dev/null +++ b/src/utils/jackets.ts @@ -0,0 +1,4 @@ +export function getJacketUrl(jacketFromData: string) { + const prefix = jacketFromData.startsWith("blob:") ? "" : "/jackets/"; + return `${prefix}${jacketFromData}`; +} From a441fd69185aa9a06909db7aefc90f6d532d705a Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sun, 28 Jul 2024 15:12:59 -0700 Subject: [PATCH 035/168] use a single urql client instacne --- src/controls/player-names.tsx | 17 +++++++---------- src/matches.tsx | 5 ++--- src/startgg-gql/index.ts | 32 ++++++++++++++++++-------------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 1e1fe4be3..7cbeda30c 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -10,11 +10,15 @@ import { import React, { useCallback } from "react"; import { useConfigState, useUpdateConfig } from "../state/hooks"; import { useIntl } from "../hooks/useIntl"; -import { atom, useAtom } from "jotai"; +import { useAtom } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; import { useAppDispatch, useAppState } from "../state/store"; import { entrantsSlice, Entrant } from "../state/entrants.slice"; -import { getEventEntrants } from "../startgg-gql"; +import { + getEventEntrants, + startggEventSlug, + startggKeyAtom, +} from "../startgg-gql"; import { Person } from "@blueprintjs/icons"; export function PlayerNamesControls() { @@ -62,13 +66,6 @@ export function PlayerNamesControls() { ); } -export const startggKeyAtom = atom( - process.env.STARTGG_TOKEN as string, -); -export const startggEventSlug = atom( - "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", -); - export function StartggEntrantManager() { const { t } = useIntl(); const entrants = useAppState(entrantsSlice.selectors.selectAll); @@ -94,7 +91,7 @@ function StartggEntrantImport() { if (!apiKey || !eventSlug) { return; } - const entrants = await getEventEntrants(apiKey, eventSlug); + const entrants = await getEventEntrants(eventSlug); dispatch(entrantsSlice.actions.upsertMany(entrants)); }, [dispatch, apiKey, eventSlug], diff --git a/src/matches.tsx b/src/matches.tsx index 50d492181..dc49fc8b4 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -1,8 +1,7 @@ import { Classes, Label, Text } from "@blueprintjs/core"; import { useAtom } from "jotai"; import { useCallback } from "react"; -import { startggKeyAtom, startggEventSlug } from "./controls/player-names"; -import { getEventSets } from "./startgg-gql"; +import { startggKeyAtom, startggEventSlug, getEventSets } from "./startgg-gql"; import { AppState, useAppDispatch, useAppState } from "./state/store"; import { setsSlice, Slot, TournamentSet } from "./state/matches.slice"; import { entrantsSlice } from "./state/entrants.slice"; @@ -52,7 +51,7 @@ function StartggSetImport() { if (!apiKey || !eventSlug) { return; } - const sets = await getEventSets(apiKey, eventSlug); + const sets = await getEventSets(eventSlug); dispatch(setsSlice.actions.upsertMany(sets)); }, [dispatch, apiKey, eventSlug], diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 444a8de10..bc7774843 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -2,21 +2,26 @@ import { Entrant } from "../state/entrants.slice"; import { TournamentSet } from "../state/matches.slice"; import { EventEntrantsDocument, EventSetsDocument } from "./generated/graphql"; import { Client, cacheExchange, fetchExchange } from "@urql/core"; +import { getDefaultStore, atom } from "jotai"; -function getClient(token: string) { - return new Client({ - url: "https://api.start.gg/gql/alpha", - fetchOptions: { - headers: { - Authorization: `Bearer ${token}`, - }, +export const startggKeyAtom = atom( + process.env.STARTGG_TOKEN as string, +); +export const startggEventSlug = atom( + "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", +); + +export const client = new Client({ + url: "https://api.start.gg/gql/alpha", + fetchOptions: () => ({ + headers: { + Authorization: `Bearer ${getDefaultStore().get(startggKeyAtom)}`, }, - exchanges: [cacheExchange, fetchExchange], - }); -} + }), + exchanges: [cacheExchange, fetchExchange], +}); -export async function getEventEntrants(token: string, slug: string) { - const client = getClient(token); +export async function getEventEntrants(slug: string) { let pageNo = 0; const ret: Entrant[] = []; @@ -43,8 +48,7 @@ export async function getEventEntrants(token: string, slug: string) { return ret; } -export async function getEventSets(token: string, slug: string) { - const client = getClient(token); +export async function getEventSets(slug: string) { let pageNo = 0; const ret: TournamentSet[] = []; From a71097265c5852a03bbc3f40de768df89782904f Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 31 Jul 2024 21:30:33 -0700 Subject: [PATCH 036/168] big steps towards startgg integration --- codegen.ts | 3 + graphql.config.ts | 2 +- package.json | 2 + src/card-draw.ts | 12 +- src/config-state.ts | 12 -- src/controls/degrs-tester.tsx | 5 +- src/controls/index.tsx | 14 +- src/controls/player-names.tsx | 41 +---- src/drawing-context.tsx | 2 + src/models/Drawing.ts | 3 +- src/song-card/song-card.tsx | 41 +---- src/startgg-gql/entrants.graphql | 23 --- src/startgg-gql/generated/fragment-masking.ts | 87 ---------- src/startgg-gql/generated/gql.ts | 18 +- src/startgg-gql/generated/graphql.ts | 20 ++- src/startgg-gql/generated/index.ts | 1 - src/startgg-gql/index.ts | 129 ++++++++++++-- src/startgg-gql/sets.graphql | 25 --- src/state/central.ts | 5 - src/state/config.slice.ts | 12 -- src/state/drawings.slice.ts | 111 +----------- src/state/store.ts | 12 +- src/state/thunks.ts | 163 ++++++++++++++++++ src/tournament-mode/drawing-actions.tsx | 34 +--- src/tournament-mode/drawing-labels.tsx | 65 +------ src/tournament-mode/round-select.tsx | 96 ----------- yarn.lock | 38 ++++ 27 files changed, 421 insertions(+), 555 deletions(-) delete mode 100644 src/startgg-gql/entrants.graphql delete mode 100644 src/startgg-gql/generated/fragment-masking.ts delete mode 100644 src/startgg-gql/sets.graphql create mode 100644 src/state/thunks.ts delete mode 100644 src/tournament-mode/round-select.tsx diff --git a/codegen.ts b/codegen.ts index 490e23ec6..3f5005a14 100644 --- a/codegen.ts +++ b/codegen.ts @@ -7,6 +7,9 @@ const config: CodegenConfig = { generates: { "./src/startgg-gql/generated/": { preset: "client", + presetConfig: { + fragmentMasking: false, + }, }, }, }; diff --git a/graphql.config.ts b/graphql.config.ts index c1648823e..ff650c77b 100644 --- a/graphql.config.ts +++ b/graphql.config.ts @@ -10,5 +10,5 @@ export default { }, }, ], - documents: ["./src/startgg-gql/*.graphql"], + documents: ["./src/startgg-gql/*.ts"], }; diff --git a/package.json b/package.json index 9cd690cf8..6ae1e366b 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@typescript-eslint/eslint-plugin": "^7.17.0", "@typescript-eslint/parser": "^7.17.0", "@urql/core": "^5.0.4", + "@urql/exchange-graphcache": "^7.1.2", "autoprefixer": "^10.4.19", "axios": "^1.7.2", "babel-loader": "^9.1.3", @@ -118,6 +119,7 @@ "style-loader": "^4.0.0", "typescript": "^5.5.3", "undici": "^6.19.4", + "urql": "^4.1.0", "victory": "^37.0.2", "webpack": "^5.93.0", "webpack-cli": "^5.1.4", diff --git a/src/card-draw.ts b/src/card-draw.ts index 7523995ca..8f4ef9383 100644 --- a/src/card-draw.ts +++ b/src/card-draw.ts @@ -225,19 +225,25 @@ function bucketIndexForLvl(lvl: number, buckets: LvlRanges): number | null { return null; } +export type StartggInfo = Pick; +export type StartingPoint = Drawing | StartggInfo; + /** * Produces a drawn set of charts given the song data and the user * input of the html form elements. * @param songs The song data (see `src/songs/`) * @param configData the data gathered by all form elements on the page, indexed by `name` attribute */ -export function draw(gameData: GameData, configData: ConfigState): Drawing { +export function draw( + gameData: GameData, + configData: ConfigState, + startPoint: StartingPoint, +): Drawing { const { chartCount: numChartsToRandom, useWeights, forceDistribution, weights, - defaultPlayersPerDraw, useGranularLevels, } = configData; @@ -389,10 +395,10 @@ export function draw(gameData: GameData, configData: ConfigState): Drawing { return { id: `draw-${nanoid(10)}`, charts, - players: times(defaultPlayersPerDraw, () => ""), bans: {}, protects: {}, pocketPicks: {}, winners: {}, + ...startPoint, }; } diff --git a/src/config-state.ts b/src/config-state.ts index 22e8fe6ac..3581f1c8a 100644 --- a/src/config-state.ts +++ b/src/config-state.ts @@ -20,8 +20,6 @@ export interface ConfigState { difficulties: Array; flags: Array; cutoffDate: string; - playerNames: string[]; - tournamentRounds: string[]; defaultPlayersPerDraw: number; sortByLevel: boolean; useGranularLevels: boolean; @@ -44,16 +42,6 @@ export const initialState: ConfigState = { folders: [], difficulties: [], flags: [], - playerNames: [], - tournamentRounds: [ - "Pools", - "Winner's Bracket", - "Winner's Finals", - "Loser's Bracket", - "Loser's Finals", - "Grand Finals", - "Tiebreaker", - ], sortByLevel: false, defaultPlayersPerDraw: 2, useGranularLevels: false, diff --git a/src/controls/degrs-tester.tsx b/src/controls/degrs-tester.tsx index dc5bd2de7..f88f73f1a 100644 --- a/src/controls/degrs-tester.tsx +++ b/src/controls/degrs-tester.tsx @@ -32,7 +32,10 @@ function* oneMillionDraws(gameData: GameData) { const configState = configSlice.selectSlice(store.getState()); for (let idx = 0; idx < TEST_SIZE; idx++) { - yield [draw(gameData, configState), idx] as const; + yield [ + draw(gameData, configState, { players: [], title: "", startggSetId: "" }), + idx, + ] as const; } } diff --git a/src/controls/index.tsx b/src/controls/index.tsx index dce5cc59e..c64a2d727 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -16,8 +16,8 @@ import { useIsNarrow } from "../hooks/useMediaQuery"; import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "../utils/error-fallback"; import { ShowChartsToggle } from "./show-charts-toggle"; -import { createDraw } from "../state/drawings.slice"; -import { useAppDispatch, useAppStore } from "../state/store"; +import { createDraw } from "../state/thunks"; +import { useAppDispatch } from "../state/store"; import { useAtomValue, useSetAtom } from "jotai"; import { showEligibleCharts } from "../config-state"; import { gameDataLoadingStatus } from "../state/game-data.atoms"; @@ -31,15 +31,19 @@ export function HeaderControls() { const hasGameData = useAtomValue(gameDataLoadingStatus) === "available"; const isNarrow = useIsNarrow(); const dispatch = useAppDispatch(); - const store = useAppStore(); function handleDraw() { setShowEligibleCharts(false); - const result = createDraw(store.getState()); + const result = dispatch( + createDraw({ + players: ["TEMP1", "TEMP2"], + title: "TEMP TITLE", + startggSetId: "PLACEHOLDER", + }), + ); if (typeof result === "boolean") { setLastDrawFailed(result); } else { - dispatch(result); setLastDrawFailed(false); } } diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 7cbeda30c..0d3209a71 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -5,7 +5,6 @@ import { Label, Text, NumericInput, - TagInput, } from "@blueprintjs/core"; import React, { useCallback } from "react"; import { useConfigState, useUpdateConfig } from "../state/hooks"; @@ -19,54 +18,18 @@ import { startggEventSlug, startggKeyAtom, } from "../startgg-gql"; -import { Person } from "@blueprintjs/icons"; export function PlayerNamesControls() { - const updateConfig = useUpdateConfig(); - const playerNames = useConfigState((s) => s.playerNames); - const { t } = useIntl(); - - function addPlayers(names: string[]) { - updateConfig((prev) => { - const next = prev.playerNames.slice(); - for (const name of names) { - if (!next.includes(name)) { - next.push(name); - } - } - if (next.length !== prev.playerNames.length) { - return { playerNames: next }; - } - return {}; - }); - } - function removePlayer(name: React.ReactNode, index: number) { - updateConfig((prev) => { - const next = prev.playerNames.slice(); - next.splice(index, 1); - return { playerNames: next }; - }); - } - return ( <> - - } - onAdd={addPlayers} - onRemove={removePlayer} - /> - + ); } -export function StartggEntrantManager() { +function StartggEntrantManager() { const { t } = useIntl(); const entrants = useAppState(entrantsSlice.selectors.selectAll); if (!entrants.length) { diff --git a/src/drawing-context.tsx b/src/drawing-context.tsx index cb875c6b7..8382a7da6 100644 --- a/src/drawing-context.tsx +++ b/src/drawing-context.tsx @@ -6,6 +6,8 @@ import { EqualityFn } from "react-redux"; const stubDrawing: Drawing = { id: "", + title: "", + startggSetId: "", bans: {}, charts: [], players: [], diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index c9b033993..3f1192480 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -41,7 +41,8 @@ export interface PocketPick extends PlayerActionOnChart { export interface Drawing { id: string; - title?: string; + startggSetId: string; + title: string; players: string[]; charts: Array; bans: Record; diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index c14f8f07f..4d86bdbb2 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -16,9 +16,10 @@ import { IconMenu } from "./icon-menu"; import { ShockBadge } from "./shock-badge"; import styles from "./song-card.css"; import { ChartLevel } from "./chart-level"; -import { useAppDispatch, useAppStore } from "../state/store"; -import { createRedrawChart, drawingsSlice } from "../state/drawings.slice"; +import { useAppDispatch } from "../state/store"; +import { createPickBanPocket, createRedrawChart } from "../state/thunks"; import { getJacketUrl } from "../utils/jackets"; +import { drawingsSlice } from "../state/drawings.slice"; const isJapanese = detectedLanguage === "ja"; @@ -48,7 +49,6 @@ export { Props as SongCardProps }; function useIconCallbacksForChart(chartId: string): IconCallbacks { const dispatch = useAppDispatch(); - const store = useAppStore(); const drawingId = useDrawing((s) => s.id); const handleBanPickPocket = useCallback( @@ -56,34 +56,8 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { type: "ban" | "protect" | "pocket", player: number, pick?: EligibleChart, - ) => { - const reorder = store.getState().config.orderByAction; - let action; - if (type === "pocket") { - if (pick) { - action = drawingsSlice.actions.banProtectReplace({ - drawingId, - chartId, - type, - player, - pick, - reorder, - }); - } - } else { - action = drawingsSlice.actions.banProtectReplace({ - drawingId, - chartId, - type, - player, - reorder, - }); - } - if (action) { - dispatch(action); - } - }, - [drawingId, chartId, dispatch, store], + ) => dispatch(createPickBanPocket(drawingId, chartId, type, player, pick)), + [drawingId, chartId, dispatch], ); return useMemo( @@ -92,8 +66,7 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { onProtect: handleBanPickPocket.bind(undefined, "protect"), onReplace: handleBanPickPocket.bind(undefined, "pocket"), onRedraw: () => { - const action = createRedrawChart(store.getState(), drawingId, chartId); - if (action) dispatch(action); + dispatch(createRedrawChart(drawingId, chartId)); }, onReset: () => dispatch(drawingsSlice.actions.resetChart({ drawingId, chartId })), @@ -102,7 +75,7 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { drawingsSlice.actions.setWinner({ drawingId, chartId, player }), ), }), - [handleBanPickPocket, store, drawingId, chartId, dispatch], + [handleBanPickPocket, drawingId, chartId, dispatch], ); } diff --git a/src/startgg-gql/entrants.graphql b/src/startgg-gql/entrants.graphql deleted file mode 100644 index f2865c392..000000000 --- a/src/startgg-gql/entrants.graphql +++ /dev/null @@ -1,23 +0,0 @@ -query EventEntrants($eventSlug: String!, $pageNo: Int!) { - event(slug: $eventSlug) { - id - name - entrants(query: { page: $pageNo, perPage: 100 }) { - pageInfo { - totalPages - } - nodes { - id - name - # paginatedSets { - # nodes { - # id - # } - # pageInfo { - # totalPages - # } - # } - } - } - } -} diff --git a/src/startgg-gql/generated/fragment-masking.ts b/src/startgg-gql/generated/fragment-masking.ts deleted file mode 100644 index aca71b135..000000000 --- a/src/startgg-gql/generated/fragment-masking.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; -import { FragmentDefinitionNode } from 'graphql'; -import { Incremental } from './graphql'; - - -export type FragmentType> = TDocumentType extends DocumentTypeDecoration< - infer TType, - any -> - ? [TType] extends [{ ' $fragmentName'?: infer TKey }] - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } - : never - : never - : never; - -// return non-nullable if `fragmentType` is non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> -): TType; -// return nullable if `fragmentType` is undefined -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | undefined -): TType | undefined; -// return nullable if `fragmentType` is nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null -): TType | null; -// return nullable if `fragmentType` is nullable or undefined -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null | undefined -): TType | null | undefined; -// return array of non-nullable if `fragmentType` is array of non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: Array>> -): Array; -// return array of nullable if `fragmentType` is array of nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: Array>> | null | undefined -): Array | null | undefined; -// return readonly array of non-nullable if `fragmentType` is array of non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> -): ReadonlyArray; -// return readonly array of nullable if `fragmentType` is array of nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> | null | undefined -): ReadonlyArray | null | undefined; -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined -): TType | Array | ReadonlyArray | null | undefined { - return fragmentType as any; -} - - -export function makeFragmentData< - F extends DocumentTypeDecoration, - FT extends ResultOf ->(data: FT, _fragment: F): FragmentType { - return data as FragmentType; -} -export function isFragmentReady( - queryNode: DocumentTypeDecoration, - fragmentNode: TypedDocumentNode, - data: FragmentType, any>> | null | undefined -): data is FragmentType { - const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ - ?.deferredFields; - - if (!deferredFields) return true; - - const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; - const fragName = fragDef?.name?.value; - - const fields = (fragName && deferredFields[fragName]) || []; - return fields.length > 0 && fields.every(field => data && field in data); -} diff --git a/src/startgg-gql/generated/gql.ts b/src/startgg-gql/generated/gql.ts index 80ff6080b..5d92f15a1 100644 --- a/src/startgg-gql/generated/gql.ts +++ b/src/startgg-gql/generated/gql.ts @@ -13,8 +13,10 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - "query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}": types.EventEntrantsDocument, - "query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}": types.EventSetsDocument, + "\n query PlayerName($pid: ID!) {\n entrant(id: $pid) {\n __typename\n id\n name\n }\n }\n": types.PlayerNameDocument, + "\n query SetName($sid: ID!) {\n set(id: $sid) {\n __typename\n id\n fullRoundText\n }\n }\n": types.SetNameDocument, + "\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n": types.EventEntrantsDocument, + "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n": types.EventSetsDocument, }; /** @@ -34,11 +36,19 @@ export function graphql(source: string): unknown; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}"): (typeof documents)["query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n name\n entrants(query: {page: $pageNo, perPage: 100}) {\n pageInfo {\n totalPages\n }\n nodes {\n id\n name\n }\n }\n }\n}"]; +export function graphql(source: "\n query PlayerName($pid: ID!) {\n entrant(id: $pid) {\n __typename\n id\n name\n }\n }\n"): (typeof documents)["\n query PlayerName($pid: ID!) {\n entrant(id: $pid) {\n __typename\n id\n name\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}"): (typeof documents)["query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: {hideEmpty: true}, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n}"]; +export function graphql(source: "\n query SetName($sid: ID!) {\n set(id: $sid) {\n __typename\n id\n fullRoundText\n }\n }\n"): (typeof documents)["\n query SetName($sid: ID!) {\n set(id: $sid) {\n __typename\n id\n fullRoundText\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/startgg-gql/generated/graphql.ts b/src/startgg-gql/generated/graphql.ts index 59cca4ff3..278aecba5 100644 --- a/src/startgg-gql/generated/graphql.ts +++ b/src/startgg-gql/generated/graphql.ts @@ -2402,13 +2402,27 @@ export type WaveUpsertInput = { startAt: Scalars['Timestamp']['input']; }; +export type PlayerNameQueryVariables = Exact<{ + pid: Scalars['ID']['input']; +}>; + + +export type PlayerNameQuery = { __typename?: 'Query', entrant?: { __typename: 'Entrant', id?: string | null, name?: string | null } | null }; + +export type SetNameQueryVariables = Exact<{ + sid: Scalars['ID']['input']; +}>; + + +export type SetNameQuery = { __typename?: 'Query', set?: { __typename: 'Set', id?: string | null, fullRoundText?: string | null } | null }; + export type EventEntrantsQueryVariables = Exact<{ eventSlug: Scalars['String']['input']; pageNo: Scalars['Int']['input']; }>; -export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, name?: string | null, entrants?: { __typename?: 'EntrantConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null } | null, nodes?: Array<{ __typename?: 'Entrant', id?: string | null, name?: string | null } | null> | null } | null } | null }; +export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename: 'Event', id?: string | null, name?: string | null, entrants?: { __typename?: 'EntrantConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null } | null, nodes?: Array<{ __typename: 'Entrant', id?: string | null, name?: string | null } | null> | null } | null } | null }; export type EventSetsQueryVariables = Exact<{ eventSlug: Scalars['String']['input']; @@ -2419,5 +2433,7 @@ export type EventSetsQueryVariables = Exact<{ export type EventSetsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, sets?: { __typename?: 'SetConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null, total?: number | null } | null, nodes?: Array<{ __typename?: 'Set', id?: string | null, fullRoundText?: string | null, identifier?: string | null, slots?: Array<{ __typename?: 'SetSlot', prereqType?: string | null, prereqId?: string | null, prereqPlacement?: number | null, entrant?: { __typename?: 'Entrant', id?: string | null, name?: string | null } | null } | null> | null } | null> | null } | null } | null }; -export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"entrants"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const PlayerNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlayerName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; +export const SetNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SetName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"set"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}}]}}]}}]} as unknown as DocumentNode; +export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"entrants"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const EventSetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventSets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hideEmpty"},"value":{"kind":"BooleanValue","value":true}}]}},{"kind":"Argument","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"slots"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prereqType"}},{"kind":"Field","name":{"kind":"Name","value":"prereqId"}},{"kind":"Field","name":{"kind":"Name","value":"prereqPlacement"}},{"kind":"Field","name":{"kind":"Name","value":"entrant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/startgg-gql/generated/index.ts b/src/startgg-gql/generated/index.ts index f51599168..af7839936 100644 --- a/src/startgg-gql/generated/index.ts +++ b/src/startgg-gql/generated/index.ts @@ -1,2 +1 @@ -export * from "./fragment-masking"; export * from "./gql"; \ No newline at end of file diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index bc7774843..70f8522bb 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -1,7 +1,14 @@ +import { useQuery } from "urql"; import { Entrant } from "../state/entrants.slice"; import { TournamentSet } from "../state/matches.slice"; -import { EventEntrantsDocument, EventSetsDocument } from "./generated/graphql"; -import { Client, cacheExchange, fetchExchange } from "@urql/core"; +import { + EventEntrantsDocument, + EventSetsDocument, + PlayerNameDocument, + SetNameDocument, +} from "./generated/graphql"; +import { Client, fetchExchange, gql } from "@urql/core"; +import { cacheExchange } from "@urql/exchange-graphcache"; import { getDefaultStore, atom } from "jotai"; export const startggKeyAtom = atom( @@ -18,9 +25,77 @@ export const client = new Client({ Authorization: `Bearer ${getDefaultStore().get(startggKeyAtom)}`, }, }), - exchanges: [cacheExchange, fetchExchange], + exchanges: [cacheExchange(), fetchExchange], }); +const PlayerNameDoc: typeof PlayerNameDocument = gql` + query PlayerName($pid: ID!) { + entrant(id: $pid) { + __typename + id + name + } + } +`; + +export function useStartggPlayerName(playerId: string) { + const [result] = useQuery({ + query: PlayerNameDoc, + variables: { + pid: playerId, + }, + }); + return result.data?.entrant?.name; +} + +const SetNameDoc: typeof SetNameDocument = gql` + query SetName($sid: ID!) { + set(id: $sid) { + __typename + id + fullRoundText + } + } +`; + +export function useStartggSetName(setId: string) { + const [result] = useQuery({ + query: SetNameDoc, + variables: { + sid: setId, + }, + }); + return result.data?.set?.fullRoundText; +} + +const EventEntrantsDoc: typeof EventEntrantsDocument = gql` + query EventEntrants($eventSlug: String!, $pageNo: Int!) { + event(slug: $eventSlug) { + __typename + id + name + entrants(query: { page: $pageNo, perPage: 100 }) { + pageInfo { + totalPages + } + nodes { + __typename + id + name + # paginatedSets { + # nodes { + # id + # } + # pageInfo { + # totalPages + # } + # } + } + } + } + } +`; + export async function getEventEntrants(slug: string) { let pageNo = 0; @@ -28,10 +103,12 @@ export async function getEventEntrants(slug: string) { let totalPages = 0; do { - const results = await client.query(EventEntrantsDocument, { - eventSlug: slug, - pageNo, - }); + const results = await client + .query(EventEntrantsDoc, { + eventSlug: slug, + pageNo, + }) + .toPromise(); totalPages = results.data?.event?.entrants?.pageInfo?.totalPages || 0; if (results.data?.event?.entrants?.nodes) { for (const entrant of results.data.event.entrants.nodes) { @@ -48,6 +125,34 @@ export async function getEventEntrants(slug: string) { return ret; } +const EventSetsDoc: typeof EventSetsDocument = gql` + query EventSets($eventSlug: String!, $pageNo: Int!) { + event(slug: $eventSlug) { + id + sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) { + pageInfo { + totalPages + total + } + nodes { + id + fullRoundText + identifier + slots { + prereqType + prereqId + prereqPlacement + entrant { + id + name + } + } + } + } + } + } +`; + export async function getEventSets(slug: string) { let pageNo = 0; @@ -55,10 +160,12 @@ export async function getEventSets(slug: string) { let totalPages = 0; do { - const results = await client.query(EventSetsDocument, { - eventSlug: slug, - pageNo, - }); + const results = await client + .query(EventSetsDoc, { + eventSlug: slug, + pageNo, + }) + .toPromise(); totalPages = results.data?.event?.sets?.pageInfo?.totalPages || 0; if (results.data?.event?.sets?.nodes) { for (const set of results.data.event.sets.nodes) { diff --git a/src/startgg-gql/sets.graphql b/src/startgg-gql/sets.graphql deleted file mode 100644 index 386e25f6c..000000000 --- a/src/startgg-gql/sets.graphql +++ /dev/null @@ -1,25 +0,0 @@ -query EventSets($eventSlug: String!, $pageNo: Int!) { - event(slug: $eventSlug) { - id - sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) { - pageInfo { - totalPages - total - } - nodes { - id - fullRoundText - identifier - slots { - prereqType - prereqId - prereqPlacement - entrant { - id - name - } - } - } - } - } -} diff --git a/src/state/central.ts b/src/state/central.ts index 6096443a1..60cf57a88 100644 --- a/src/state/central.ts +++ b/src/state/central.ts @@ -6,8 +6,3 @@ export const receivePartyState = createAction( "party/supplyState", withPayload(), ); - -export const addPlayerNameToDrawing = createAction( - "players/addToDrawing", - withPayload<{ name: string; asPlayerNo: number; drawingId: string }>(), -); diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index 65e075517..5a36ca856 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { ConfigState, initialState } from "../config-state"; import { GameData } from "../models/SongData"; -import { addPlayerNameToDrawing } from "./central"; export const configSlice = createSlice({ name: "config", @@ -32,15 +31,4 @@ export const configSlice = createSlice({ Object.assign(state, patch); }, }, - extraReducers(builder) { - builder.addCase(addPlayerNameToDrawing, (state, action) => { - if (!action.payload.name) { - return; - } - if (state.playerNames.includes(action.payload.name)) { - return; - } - state.playerNames.push(action.payload.name); - }); - }, }); diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index f7d86ffbc..9931dd2b3 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -9,53 +9,9 @@ import { EligibleChart, PlayerActionOnChart, } from "../models/Drawing"; -import { addPlayerNameToDrawing } from "./central"; -import { AppState } from "./store"; -import { draw } from "../card-draw"; -import { getDefaultStore } from "jotai"; -import { gameDataAtom } from "./game-data.atoms"; export const drawingsAdapter = createEntityAdapter({}); -declare const umami: { - track( - eventName?: string, - eventProperties?: Record, - ): void; -}; - -function trackDraw(count: number | null, game?: string) { - if (typeof umami === "undefined") { - return; - } - const results = - count === null ? { result: "failed" } : { result: "success", count, game }; - umami.track("cards-drawn", results); -} - -/** - * Performs a draw and returns an action to be dispatched - * @returns false if no draw was possible, and - * true if fewer charts were drawn than requested, - * addDrawing action in the default successful case - */ -export function createDraw(state: AppState) { - const jotaiStore = getDefaultStore(); - const gameData = jotaiStore.get(gameDataAtom); - if (!gameData) { - trackDraw(null); - return false; - } - - const drawing = draw(gameData, state.config); - trackDraw(drawing.charts.length, state.gameData.dataSetName); - if (!drawing.charts.length) { - return true; - } - - return drawingsSlice.actions.addDrawing(drawing); -} - /** payload is the drawing id */ type ActionOnSingleDrawing = PayloadAction; // eslint-disable-next-line @typescript-eslint/ban-types @@ -95,15 +51,12 @@ export const drawingsSlice = createSlice({ } Object.assign(chart, action.payload.changes); }, - addEmptyPlayer(state, action: ActionOnSingleDrawing) { + swapPlayerPositions(state, action: ActionOnSingleDrawing) { const drawing = state.entities[action.payload]; - if (!drawing) return; - drawing.players.push(""); - }, - dropPlayer(state, action: ActionOnSingleDrawing) { - const drawing = state.entities[action.payload]; - if (!drawing) return; - drawing.players.pop(); + if (!drawing) { + return; + } + drawing.players = drawing.players.toReversed(); }, incrementPriorityPlayer(state, action: ActionOnSingleDrawing) { const drawing = state.entities[action.payload]; @@ -174,15 +127,6 @@ export const drawingsSlice = createSlice({ } }, }, - extraReducers(builder) { - builder.addCase(addPlayerNameToDrawing, (state, action) => { - const drawing = state.entities[action.payload.drawingId]; - if (!drawing) { - return; - } - drawing.players[action.payload.asPlayerNo - 1] = action.payload.name; - }); - }, selectors: { haveDrawings(state) { return !!state.ids.length; @@ -194,51 +138,6 @@ export const drawingSelectors = drawingsAdapter.getSelectors( drawingsSlice.selectSlice, ); -export function createRedrawAll(state: AppState, drawingId: string) { - // TODO preserve protects and pocket picks in this logic - const drawing = state.drawings.entities[drawingId]; - const drawConfig = { - ...state.config, - chartCount: drawing.charts.length, - }; - const jotaiStore = getDefaultStore(); - const gameData = jotaiStore.get(gameDataAtom); - const drawResult = draw(gameData!, drawConfig); - return drawingsSlice.actions.updateOne({ - id: drawingId, - changes: { - charts: drawResult.charts, - pocketPicks: {}, - bans: {}, - protects: {}, - winners: {}, - }, - }); -} - -export function createRedrawChart( - state: AppState, - drawingId: string, - chartId: string, -) { - const drawConfig = { - ...state.config, - chartCount: 1, - }; - const jotaiStore = getDefaultStore(); - const gameData = jotaiStore.get(gameDataAtom); - const drawResult = draw(gameData!, drawConfig); - const chart = drawResult.charts.find((c) => c.type === "DRAWN"); - if (!chart) { - return; - } - return drawingsSlice.actions.updateOneChart({ - drawingId, - chartId, - changes: chart, - }); -} - function moveChartInArray( drawing: Drawing, chartId: string, diff --git a/src/state/store.ts b/src/state/store.ts index 058c3f6b4..f74555f20 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -1,4 +1,8 @@ -import { configureStore } from "@reduxjs/toolkit"; +import { + configureStore, + ThunkAction, + ActionFromReducer, +} from "@reduxjs/toolkit"; import { useDispatch, useSelector, useStore } from "react-redux"; import { reducer } from "./root-reducer"; import { listenerMiddleware } from "./listener-middleware"; @@ -14,3 +18,9 @@ export const useAppState = useSelector.withTypes(); export type AppDispatch = typeof store.dispatch; export const useAppDispatch = useDispatch.withTypes(); export const useAppStore = useStore.withTypes(); +export type AppThunk = ThunkAction< + ReturnType, + AppState, + unknown, + ActionFromReducer +>; diff --git a/src/state/thunks.ts b/src/state/thunks.ts new file mode 100644 index 000000000..8232ca849 --- /dev/null +++ b/src/state/thunks.ts @@ -0,0 +1,163 @@ +import { AppThunk } from "./store"; +import { draw, StartggInfo } from "../card-draw"; +import { getDefaultStore } from "jotai"; +import { gameDataAtom } from "./game-data.atoms"; +import { drawingsSlice } from "./drawings.slice"; +import { EligibleChart } from "../models/Drawing"; + +declare const umami: { + track( + eventName?: string, + eventProperties?: Record, + ): void; +}; + +function trackDraw(count: number | null, game?: string) { + if (typeof umami === "undefined") { + return; + } + const results = + count === null ? { result: "failed" } : { result: "success", count, game }; + umami.track("cards-drawn", results); +} + +/** + * Thunk creator for performing a new draw + * @returns false if draw was unsuccessful + */ +export function createDraw(startggTargetSet: StartggInfo): AppThunk { + return (dispatch, getState) => { + const state = getState(); + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + if (!gameData) { + trackDraw(null); + return false; // no draw was possible + } + + const drawing = draw(gameData, state.config, startggTargetSet); + trackDraw(drawing.charts.length, state.gameData.dataSetName); + if (!drawing.charts.length) { + return false; // could not draw the requested number of charts + } + + dispatch(drawingsSlice.actions.addDrawing(drawing)); + }; +} + +/** + * thunk creator for redrawing all charts in a target drawing + */ +export function createRedrawAll(drawingId: string): AppThunk { + return (dispatch, getState) => { + const state = getState(); + const drawing = state.drawings.entities[drawingId]; + const drawConfig = { + ...state.config, + chartCount: drawing.charts.length, + }; + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + + // preserve pocket picks and protects in the redraw by keeping them in the starting point info + // and filtering out all other charts + const protectedChartIds = new Set( + Object.keys(drawing.pocketPicks).concat(Object.keys(drawing.protects)), + ); + const startingPoint = { + ...drawing, + charts: drawing.charts.filter( + (chart) => + protectedChartIds.has(chart.id) || chart.type === "PLACEHOLDER", + ), + }; + + const drawResult = draw(gameData!, drawConfig, startingPoint); + dispatch( + drawingsSlice.actions.updateOne({ + id: drawingId, + changes: { + charts: drawResult.charts, + pocketPicks: drawing.pocketPicks, + bans: {}, + protects: drawing.protects, + winners: {}, + }, + }), + ); + }; +} + +/** + * thunk creator for redrawing a single chart within a drawing + */ +export function createRedrawChart( + drawingId: string, + chartId: string, +): AppThunk { + return (dispatch, getState) => { + const jotaiStore = getDefaultStore(); + const gameData = jotaiStore.get(gameDataAtom); + if (!gameData) return; + const state = getState(); + const drawing = state.drawings.entities[drawingId]; + const startingPoint = { + ...drawing, + charts: drawing.charts.filter((chart) => chart.id !== chartId), + }; + + const drawResult = draw(gameData, state.config, startingPoint); + const chart = drawResult.charts.pop(); + if ( + !chart || + chart.type !== "DRAWN" || + drawing.charts.some((c) => c.id === chart.id) + ) { + return; // result didn't include a new chart + } + dispatch( + drawingsSlice.actions.updateOneChart({ + drawingId, + chartId, + changes: chart, + }), + ); + }; +} + +/** thunk creator for pick/ban/pocket pick that can include orderByAction setting */ +export function createPickBanPocket( + drawingId: string, + chartId: string, + type: "ban" | "protect" | "pocket", + player: number, + pick?: EligibleChart, +): AppThunk { + return (dispatch, getState) => { + const reorder = getState().config.orderByAction; + let action; + if (type === "pocket") { + if (pick) { + action = drawingsSlice.actions.banProtectReplace({ + drawingId, + chartId, + type, + player, + pick, + reorder, + }); + } + } else { + action = drawingsSlice.actions.banProtectReplace({ + drawingId, + chartId, + type, + player, + reorder, + }); + } + if (action) { + dispatch(action); + } + }; +} diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index 5594a8d3d..5971be41d 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -1,22 +1,16 @@ import { Button, Menu, MenuItem, Popover, Tooltip } from "@blueprintjs/core"; -import { - Camera, - Refresh, - NewPerson, - BlockedPerson, - Error, - CubeAdd, -} from "@blueprintjs/icons"; +import { Camera, Refresh, Error, CubeAdd, Exchange } from "@blueprintjs/icons"; import { useDrawing } from "../drawing-context"; import styles from "./drawing-actions.css"; import { domToPng } from "modern-screenshot"; import { shareImage } from "../utils/share"; import { useErrorBoundary } from "react-error-boundary"; -import { useAppDispatch, useAppState, useAppStore } from "../state/store"; +import { useAppDispatch, useAppState } from "../state/store"; import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; -import { createRedrawAll, drawingsSlice } from "../state/drawings.slice"; +import { drawingsSlice } from "../state/drawings.slice"; import { eventSlice } from "../state/event.slice"; +import { createRedrawAll } from "../state/thunks"; const DEFAULT_FILENAME = "card-draw.png"; @@ -24,10 +18,8 @@ export function DrawingActions() { const dispatch = useAppDispatch(); const cabs = useAppState(eventSlice.selectors.allCabs); const drawingId = useDrawing((s) => s.id); - const hasPlayers = useDrawing((s) => !!s.players.length); const showLabels = useAtomValue(showPlayerAndRoundLabels); const { showBoundary } = useErrorBoundary(); - const store = useAppStore(); const addToCabMenu = ( @@ -76,7 +68,7 @@ export function DrawingActions() { onClick={() => confirm( "This will replace everything besides protects and picks!", - ) && dispatch(createRedrawAll(store.getState(), drawingId)) + ) && dispatch(createRedrawAll(drawingId)) } /> @@ -94,22 +86,12 @@ export function DrawingActions() { )} {showLabels && ( <> - + + + ); +} + function StartggSetImport() { const [apiKey, setApiKey] = useAtom(startggKeyAtom); const [eventSlug, setEventSlug] = useAtom(startggEventSlug); diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 70f8522bb..b4720db78 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -9,7 +9,7 @@ import { } from "./generated/graphql"; import { Client, fetchExchange, gql } from "@urql/core"; import { cacheExchange } from "@urql/exchange-graphcache"; -import { getDefaultStore, atom } from "jotai"; +import { getDefaultStore, atom, useAtomValue } from "jotai"; export const startggKeyAtom = atom( process.env.STARTGG_TOKEN as string, @@ -18,7 +18,7 @@ export const startggEventSlug = atom( "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", ); -export const client = new Client({ +export const urqlClient = new Client({ url: "https://api.start.gg/gql/alpha", fetchOptions: () => ({ headers: { @@ -68,6 +68,17 @@ export function useStartggSetName(setId: string) { return result.data?.set?.fullRoundText; } +export function useStartggMatches() { + const eventSlug = useAtomValue(startggEventSlug)!; + return useQuery({ + query: EventSetsDoc, + variables: { + eventSlug, + pageNo: 0, + }, + }); +} + const EventEntrantsDoc: typeof EventEntrantsDocument = gql` query EventEntrants($eventSlug: String!, $pageNo: Int!) { event(slug: $eventSlug) { @@ -103,7 +114,7 @@ export async function getEventEntrants(slug: string) { let totalPages = 0; do { - const results = await client + const results = await urqlClient .query(EventEntrantsDoc, { eventSlug: slug, pageNo, @@ -160,7 +171,7 @@ export async function getEventSets(slug: string) { let totalPages = 0; do { - const results = await client + const results = await urqlClient .query(EventSetsDoc, { eventSlug: slug, pageNo, diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 9931dd2b3..2679c593b 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -131,6 +131,11 @@ export const drawingsSlice = createSlice({ haveDrawings(state) { return !!state.ids.length; }, + associatedMatchIds(state) { + return Object.values(state.entities).map( + (drawing) => drawing.startggSetId, + ); + }, }, }); From 935ec73c41386b045d20062abe401bac3e5fabd3 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 12 Aug 2024 17:16:57 -0700 Subject: [PATCH 044/168] display url example inline --- src/matches.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/matches.tsx b/src/matches.tsx index bc7db0682..8e4579fa4 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -158,7 +158,10 @@ export function StartggApiKeyGated(props: { children: ReactNode }) { From 2359225081a4cced216afb379cc7afdb401ece7b Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 12 Aug 2024 17:18:29 -0700 Subject: [PATCH 045/168] pre-populate initial values for startgg inputs --- src/matches.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/matches.tsx b/src/matches.tsx index 8e4579fa4..61f999ff1 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -154,7 +154,12 @@ export function StartggApiKeyGated(props: { children: ReactNode }) { create a personal token here - ) + ){" "} + From 95384a9d4375c5dd292646f5195f3f94318a4766 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 12 Aug 2024 17:40:26 -0700 Subject: [PATCH 046/168] persist startgg event info to localstorage --- src/startgg-gql/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index b4720db78..52c05be45 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -9,13 +9,20 @@ import { } from "./generated/graphql"; import { Client, fetchExchange, gql } from "@urql/core"; import { cacheExchange } from "@urql/exchange-graphcache"; -import { getDefaultStore, atom, useAtomValue } from "jotai"; +import { getDefaultStore, useAtomValue } from "jotai"; +import { atomWithStorage } from "jotai/utils"; -export const startggKeyAtom = atom( +export const startggKeyAtom = atomWithStorage( + "ddrtools.event.startggtoken", process.env.STARTGG_TOKEN as string, + undefined, + { getOnInit: true }, ); -export const startggEventSlug = atom( +export const startggEventSlug = atomWithStorage( + "ddrtools.event.startggslug", "tournament/red-october-2024/event/stepmaniax-singles-hard-and-wild", + undefined, + { getOnInit: true }, ); export const urqlClient = new Client({ From 39af9fa2873043ce510c9f617ce8dd9f65906f78 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 14 Aug 2024 16:28:51 -0700 Subject: [PATCH 047/168] fix swapping player positions by totally blowing up the drawing model --- src/cab-management.tsx | 7 ++-- src/card-draw.ts | 8 +++- src/controls/degrs-tester.tsx | 4 +- src/controls/index.tsx | 9 +++-- src/drawing-context.tsx | 9 +++-- src/matches.tsx | 7 +++- src/models/Drawing.ts | 43 ++++++++++++++++++-- src/obs-sources/text.tsx | 7 +++- src/song-card/card-label.tsx | 8 ++-- src/song-card/icon-menu.tsx | 14 ++++--- src/song-card/song-card.tsx | 55 ++++++++++++++------------ src/song-card/use-player-label.ts | 5 ++- src/state/drawings.slice.ts | 12 +++--- src/tournament-mode/drawing-labels.tsx | 13 +++--- 14 files changed, 137 insertions(+), 64 deletions(-) diff --git a/src/cab-management.tsx b/src/cab-management.tsx index 9590e3e7e..6421f7818 100644 --- a/src/cab-management.tsx +++ b/src/cab-management.tsx @@ -24,6 +24,7 @@ import { detectedLanguage } from "./utils"; import { copyPlainTextToClipboard } from "./utils/share"; import { useSetAtom } from "jotai"; import { mainTabAtom } from "./main-view"; +import { playerNameByIndex } from "./models/Drawing"; export function CabManagement() { const cabs = useAppState(eventSlice.selectors.allCabs); @@ -181,8 +182,8 @@ function CurrentMatch(props: { cab: CabInfo }) { if (!drawing) { return

No match

; } - const filledPlayers = drawing.players.map( - (p, idx) => p || `Player ${idx + 1}`, + const filledPlayers = drawing.playerDisplayOrder.map((pIdx, idx) => + playerNameByIndex(drawing.meta, pIdx, `Player ${idx + 1}`), ); return ( -

{drawing.title}

+

{drawing.meta.title}

{listFormatter.format(filledPlayers)}

); diff --git a/src/card-draw.ts b/src/card-draw.ts index 20d54ec6d..5a4efaefd 100644 --- a/src/card-draw.ts +++ b/src/card-draw.ts @@ -225,7 +225,7 @@ function bucketIndexForLvl(lvl: number, buckets: LvlRanges): number | null { return null; } -export type StartggInfo = Pick; +export type StartggInfo = Pick; export type StartingPoint = Drawing | StartggInfo; const artistDrawBlocklist = new Set(["Carlito", "Dr. Bombay"]); @@ -432,12 +432,18 @@ export function draw( ); } + const players = + startPoint.meta.type === "simple" + ? startPoint.meta.players + : startPoint.meta.entrants; + return { id: `draw-${nanoid(10)}`, bans: {}, protects: {}, pocketPicks: {}, winners: {}, + playerDisplayOrder: players.map((_, idx) => idx), ...startPoint, charts, }; diff --git a/src/controls/degrs-tester.tsx b/src/controls/degrs-tester.tsx index f88f73f1a..82ce55f23 100644 --- a/src/controls/degrs-tester.tsx +++ b/src/controls/degrs-tester.tsx @@ -33,7 +33,9 @@ function* oneMillionDraws(gameData: GameData) { for (let idx = 0; idx < TEST_SIZE; idx++) { yield [ - draw(gameData, configState, { players: [], title: "", startggSetId: "" }), + draw(gameData, configState, { + meta: { players: [], title: "", type: "simple" }, + }), idx, ] as const; } diff --git a/src/controls/index.tsx b/src/controls/index.tsx index 47b9af62f..d09c44b8d 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -40,9 +40,12 @@ export function HeaderControls() { setShowEligibleCharts(false); const result = dispatch( createDraw({ - players: match.players, - title: match.title, - startggSetId: match.id, + meta: { + type: "startgg", + entrants: match.players, + title: match.title, + id: match.id, + }, }), ); if (typeof result === "boolean") { diff --git a/src/drawing-context.tsx b/src/drawing-context.tsx index 8382a7da6..dcfb10ea7 100644 --- a/src/drawing-context.tsx +++ b/src/drawing-context.tsx @@ -6,11 +6,14 @@ import { EqualityFn } from "react-redux"; const stubDrawing: Drawing = { id: "", - title: "", - startggSetId: "", + meta: { + type: "simple", + players: [], + title: "", + }, + playerDisplayOrder: [], bans: {}, charts: [], - players: [], pocketPicks: {}, protects: {}, winners: {}, diff --git a/src/matches.tsx b/src/matches.tsx index 61f999ff1..f0957d4ad 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -29,7 +29,7 @@ export function MatchListAndSettings() { export interface PickedMatch { title: string; - players: string[]; + players: Array<{ id: string; name: string }>; id: string; } @@ -91,7 +91,10 @@ export function MatchPicker(props: { onPickMatch?(match: PickedMatch): void }) { : () => props.onPickMatch?.({ title, - players: [p1!, p2!], + players: match.slots!.map((slot) => ({ + id: slot!.entrant!.id!, + name: inferShortname(slot!.entrant!.name)!, + })), id: match.id!, }) } diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index 3f1192480..7ba477f0c 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -39,15 +39,52 @@ export interface PocketPick extends PlayerActionOnChart { pick: EligibleChart; } -export interface Drawing { +export interface StartggMeta { + type: "startgg"; + /** id of the set */ id: string; - startggSetId: string; title: string; + entrants: Array<{ id: string; name: string }>; +} + +export interface SimpleMeta { + type: "simple"; + title: string; + /** plain player names */ players: string[]; +} + +export function playerNameByDisplayPos( + d: Pick, + pos: number, +) { + const playerIndex = d.playerDisplayOrder[pos - 1]; + return playerNameByIndex(d.meta, playerIndex); +} + +export function playerNameByIndex( + meta: Drawing["meta"], + idx: number, + fallback = `P${idx + 1}`, +) { + switch (meta.type) { + case "simple": + return meta.players[idx] || fallback; + case "startgg": + return meta.entrants[idx].name || fallback; + } +} + +export interface Drawing { + id: string; + meta: SimpleMeta | StartggMeta; + /** index of items of the players array, in the order they should be displayed */ + playerDisplayOrder: number[]; + /** map of song ID to player index or id */ + winners: Record; charts: Array; bans: Record; protects: Record; - winners: Record; pocketPicks: Record; priorityPlayer?: number; } diff --git a/src/obs-sources/text.tsx b/src/obs-sources/text.tsx index 3cbc31020..6cdba4dea 100644 --- a/src/obs-sources/text.tsx +++ b/src/obs-sources/text.tsx @@ -1,13 +1,14 @@ import { useParams } from "react-router-dom"; import { drawingSelectors } from "../state/drawings.slice"; import { useAppState } from "../state/store"; +import { playerNameByIndex } from "../models/Drawing"; export function CabTitle() { const params = useParams<"roomName" | "cabId">(); const text = useAppState((s) => { const drawingId = s.event.cabs[params.cabId!].activeMatch; if (!drawingId) return null; - return drawingSelectors.selectById(s, drawingId).title; + return drawingSelectors.selectById(s, drawingId).meta.title; }); return

{text}

; } @@ -17,7 +18,9 @@ export function CabPlayer(props: { p: number }) { const text = useAppState((s) => { const drawingId = s.event.cabs[params.cabId!].activeMatch; if (!drawingId) return null; - return drawingSelectors.selectById(s, drawingId).players[props.p - 1]; + const drawing = drawingSelectors.selectById(s, drawingId); + const playerIndex = drawing.playerDisplayOrder[props.p - 1]; + return playerNameByIndex(drawing.meta, playerIndex, ""); }); return

{text}

; } diff --git a/src/song-card/card-label.tsx b/src/song-card/card-label.tsx index a1c3c05bb..6806646f1 100644 --- a/src/song-card/card-label.tsx +++ b/src/song-card/card-label.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Intent, Tag } from "@blueprintjs/core"; import styles from "./card-label.css"; import { Inheritance, BanCircle, Lock, Crown, Draw } from "@blueprintjs/icons"; -import { usePlayerLabel } from "./use-player-label"; +import { usePlayerLabelForIndex } from "./use-player-label"; export enum LabelType { Protect = 1, @@ -14,7 +14,7 @@ export enum LabelType { } interface Props { - player: number; + playerIdx: number; type: LabelType; onRemove?: () => void; } @@ -49,8 +49,8 @@ function getIcon(type: LabelType) { } } -export function CardLabel({ player, type, onRemove }: Props) { - const label = usePlayerLabel(player); +export function CardLabel({ playerIdx, type, onRemove }: Props) { + const label = usePlayerLabelForIndex(playerIdx); const rootClassname = classNames(styles.cardLabel, { [styles.winner]: type === LabelType.Winner, diff --git a/src/song-card/icon-menu.tsx b/src/song-card/icon-menu.tsx index f673ea922..705d0f386 100644 --- a/src/song-card/icon-menu.tsx +++ b/src/song-card/icon-menu.tsx @@ -9,6 +9,7 @@ import { } from "@blueprintjs/icons"; import { Menu, MenuItem, MenuDivider } from "@blueprintjs/core"; import { useDrawing } from "../drawing-context"; +import { playerNameByIndex } from "../models/Drawing"; interface Props { onStartPocketPick?: (p: number) => void; @@ -74,14 +75,17 @@ interface IconRowProps { } function PlayerList({ icon, text, onClick }: IconRowProps) { - const players = useDrawing((d) => d.players); + const drawingMeta = useDrawing((d) => d.meta); + const players = useDrawing((d) => d.playerDisplayOrder).map( + (pIdx) => [playerNameByIndex(drawingMeta, pIdx), pIdx] as const, + ); return ( - {players.map((p, idx) => ( + {players.map(([playerName, pIdx]) => ( onClick(idx + 1)} + key={pIdx} + text={playerName} + onClick={() => onClick(pIdx)} icon={} /> ))} diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index 4d86bdbb2..a0c427969 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -23,24 +23,24 @@ import { drawingsSlice } from "../state/drawings.slice"; const isJapanese = detectedLanguage === "ja"; -type Player = number; +type PlayerIdx = number; interface IconCallbacks { - onVeto: (p: Player) => void; - onProtect: (p: Player) => void; - onReplace: (p: Player, chart: EligibleChart) => void; + onVeto: (p: PlayerIdx) => void; + onProtect: (p: PlayerIdx) => void; + onReplace: (p: PlayerIdx, chart: EligibleChart) => void; onRedraw: () => void; onReset: () => void; - onSetWinner: (p: Player | null) => void; + onSetWinner: (p: PlayerIdx | null) => void; } interface Props { onClick?: () => void; chart: DrawnChart | EligibleChart | PlayerPickPlaceholder; - vetoedBy?: Player; - protectedBy?: Player; - replacedBy?: Player; - winner?: Player | null; + vetoedBy?: PlayerIdx; + protectedBy?: PlayerIdx; + replacedBy?: PlayerIdx; + winner?: PlayerIdx | null; replacedWith?: EligibleChart; actionsEnabled?: boolean; } @@ -125,7 +125,12 @@ export function SongCard(props: Props) { dateAdded, } = replacedWith || baseChartValues(chart); - const hasLabel = !!(vetoedBy || protectedBy || replacedBy); + const hasLabel = !!( + vetoedBy !== undefined || + protectedBy !== undefined || + replacedBy !== undefined + ); + const hasWinner = typeof winner === "number"; let jacketBg = {}; if (jacket) { @@ -137,8 +142,8 @@ export function SongCard(props: Props) { const iconCallbacks = useIconCallbacksForChart((chart as DrawnChart).id); let menuContent: undefined | JSX.Element; - if (actionsEnabled && !winner) { - if (!replacedWith && baseChartIsPlaceholder) { + if (actionsEnabled && !hasWinner) { + if (replacedWith !== undefined && baseChartIsPlaceholder) { menuContent = ( ); @@ -152,16 +157,16 @@ export function SongCard(props: Props) { onSetWinner={iconCallbacks.onSetWinner} /> ); - } else if (!vetoedBy) { + } else if (vetoedBy === undefined) { menuContent = ; } } const rootClassname = classNames(styles.chart, { - [styles.vetoed]: vetoedBy, - [styles.protected]: protectedBy, - [styles.replaced]: replacedBy && !baseChartIsPlaceholder, - [styles.picked]: replacedBy && baseChartIsPlaceholder, + [styles.vetoed]: vetoedBy !== undefined, + [styles.protected]: protectedBy !== undefined, + [styles.replaced]: replacedBy !== undefined && !baseChartIsPlaceholder, + [styles.picked]: replacedBy !== undefined && baseChartIsPlaceholder, [styles.clickable]: !!menuContent || !!props.onClick, [styles.hideVeto]: hideVetos, }); @@ -187,32 +192,32 @@ export function SongCard(props: Props) { onCancel={() => setPocketPickPendingForPlayer(0)} />
- {vetoedBy && ( + {vetoedBy !== undefined && ( )} - {protectedBy && ( + {protectedBy !== undefined && ( )} - {replacedBy && ( + {replacedBy !== undefined && ( )} - {winner && ( + {hasWinner && ( iconCallbacks?.onSetWinner(null)} /> diff --git a/src/song-card/use-player-label.ts b/src/song-card/use-player-label.ts index 6dc193d03..b3ecd1f9d 100644 --- a/src/song-card/use-player-label.ts +++ b/src/song-card/use-player-label.ts @@ -1,5 +1,6 @@ import { useDrawing } from "../drawing-context"; +import { playerNameByIndex } from "../models/Drawing"; -export function usePlayerLabel(n: number) { - return useDrawing((d) => d.players[n - 1] || `P${n}`); +export function usePlayerLabelForIndex(pIdx: number) { + return useDrawing((d) => playerNameByIndex(d.meta, pIdx)); } diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 2679c593b..41f27fdca 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -56,7 +56,7 @@ export const drawingsSlice = createSlice({ if (!drawing) { return; } - drawing.players = drawing.players.toReversed(); + drawing.playerDisplayOrder = drawing.playerDisplayOrder.toReversed(); }, incrementPriorityPlayer(state, action: ActionOnSingleDrawing) { const drawing = state.entities[action.payload]; @@ -68,7 +68,7 @@ export const drawingsSlice = createSlice({ priorityPlayer = 1; } else { priorityPlayer += 1; - if (priorityPlayer >= drawing.players.length + 1) { + if (priorityPlayer >= drawing.playerDisplayOrder.length + 1) { priorityPlayer = undefined; } } @@ -132,9 +132,11 @@ export const drawingsSlice = createSlice({ return !!state.ids.length; }, associatedMatchIds(state) { - return Object.values(state.entities).map( - (drawing) => drawing.startggSetId, - ); + return Object.values(state.entities) + .map((drawing) => drawing.meta.type === "startgg" && drawing.meta.id) + .filter( + (value): value is string => typeof value === "string" && !!value, + ); }, }, }); diff --git a/src/tournament-mode/drawing-labels.tsx b/src/tournament-mode/drawing-labels.tsx index e4706961a..6b403bbc3 100644 --- a/src/tournament-mode/drawing-labels.tsx +++ b/src/tournament-mode/drawing-labels.tsx @@ -7,22 +7,25 @@ import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; import { useAppDispatch } from "../state/store"; import { drawingsSlice } from "../state/drawings.slice"; +import { playerNameByDisplayPos } from "../models/Drawing"; export function SetLabels() { const showLabels = useAtomValue(showPlayerAndRoundLabels); - const players = useDrawing((s) => s.players); - const title = useDrawing((s) => s.title); + const playerDisplayOrder = useDrawing((d) => d.playerDisplayOrder); + const meta = useDrawing((d) => d.meta); if (!showLabels) { return null; } + const psuedoDrawing = { meta, playerDisplayOrder }; + return (
-
{title}
+
{meta.title}
- {players[0]} + {playerNameByDisplayPos(psuedoDrawing, 1)} - {players[1]} + {playerNameByDisplayPos(psuedoDrawing, 2)}
); From d365278d738894504a28018498fa89af61aa7fb0 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 16 Aug 2024 19:57:53 -0700 Subject: [PATCH 048/168] added working reporting of results to startgg --- src/controls/index.tsx | 3 +- src/controls/player-names.tsx | 178 ++++++++---------------- src/main-view.tsx | 6 +- src/matches.tsx | 151 ++------------------ src/models/Drawing.ts | 2 +- src/startgg-gql/components.tsx | 65 +++++++++ src/startgg-gql/generated/gql.ts | 5 + src/startgg-gql/generated/graphql.ts | 12 +- src/startgg-gql/index.ts | 141 +++++++------------ src/state/entrants.slice.ts | 27 ---- src/state/matches.slice.ts | 38 ----- src/state/root-reducer.ts | 4 - src/tournament-mode/drawing-actions.tsx | 122 ++++++++++++++-- 13 files changed, 319 insertions(+), 435 deletions(-) create mode 100644 src/startgg-gql/components.tsx delete mode 100644 src/state/entrants.slice.ts delete mode 100644 src/state/matches.slice.ts diff --git a/src/controls/index.tsx b/src/controls/index.tsx index d09c44b8d..f6e23f16e 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -23,7 +23,8 @@ import { useAppDispatch } from "../state/store"; import { useAtomValue, useSetAtom } from "jotai"; import { showEligibleCharts } from "../config-state"; import { gameDataLoadingStatus } from "../state/game-data.atoms"; -import { MatchPicker, PickedMatch, StartggApiKeyGated } from "../matches"; +import { MatchPicker, PickedMatch } from "../matches"; +import { StartggApiKeyGated } from "../startgg-gql/components"; const ControlsDrawer = lazy(() => import("./controls-drawer")); diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 9f0bc22c4..96e68d8af 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -1,136 +1,78 @@ -import { - Checkbox, - Classes, - FormGroup, - Label, - Text, - NumericInput, -} from "@blueprintjs/core"; -import React, { useCallback } from "react"; -import { useConfigState, useUpdateConfig } from "../state/hooks"; -import { useIntl } from "../hooks/useIntl"; -import { useAtom } from "jotai"; -import { showPlayerAndRoundLabels } from "../config-state"; -import { useAppDispatch, useAppState } from "../state/store"; -import { entrantsSlice, Entrant } from "../state/entrants.slice"; -import { - getEventEntrants, - startggEventSlug, - startggKeyAtom, -} from "../startgg-gql"; +import { Section, SectionCard } from "@blueprintjs/core"; +// import { useConfigState, useUpdateConfig } from "../state/hooks"; +// import { useIntl } from "../hooks/useIntl"; +import { useAtomValue } from "jotai"; +// import { showPlayerAndRoundLabels } from "../config-state"; +// import { useAppState } from "../state/store"; +import { startggEventSlug, startggKeyAtom } from "../startgg-gql"; +import { StartggCredsManager } from "../startgg-gql/components"; export function PlayerNamesControls() { + const apiKey = useAtomValue(startggKeyAtom); + const eventSlug = useAtomValue(startggEventSlug); return ( <> - - - +
+ + + +
); } -function StartggEntrantManager() { - const { t } = useIntl(); - const entrants = useAppState(entrantsSlice.selectors.selectAll); - if (!entrants.length) { - return ; - } - return ( - - {entrants.map((e) => ( - - ))} - - ); -} - -function StartggEntrantImport() { - const [apiKey, setApiKey] = useAtom(startggKeyAtom); - const [eventSlug, setEventSlug] = useAtom(startggEventSlug); - const dispatch = useAppDispatch(); - const importEntrants = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!apiKey || !eventSlug) { - return; - } - const entrants = await getEventEntrants(eventSlug); - dispatch(entrantsSlice.actions.upsertMany(entrants)); - }, - [dispatch, apiKey, eventSlug], - ); - return ( -
- No players added yet, import from start.gg - - - -
- ); -} - export function inferShortname(name: string | null | undefined) { if (!name) return; const namePieces = name.split(" | "); return namePieces.length >= 1 ? namePieces[namePieces.length - 1] : undefined; } -function EntrantNameForm(props: { entrant: Entrant }) { - return ( - - ); -} +// function EntrantNameForm(props: { entrant: Entrant }) { +// return ( +// +// ); +// } -function ShowLabelsToggle() { - const [enabled, updateShowLabels] = useAtom(showPlayerAndRoundLabels); - const { t } = useIntl(); +// function ShowLabelsToggle() { +// const [enabled, updateShowLabels] = useAtom(showPlayerAndRoundLabels); +// const { t } = useIntl(); - return ( - updateShowLabels(e.currentTarget.checked)} - label={t("controls.playerLabels")} - /> - ); -} +// return ( +// updateShowLabels(e.currentTarget.checked)} +// label={t("controls.playerLabels")} +// /> +// ); +// } -function PlayersPerDraw() { - const update = useUpdateConfig(); - const ppd = useConfigState((s) => s.defaultPlayersPerDraw); - const { t } = useIntl(); +// function PlayersPerDraw() { +// const update = useUpdateConfig(); +// const ppd = useConfigState((s) => s.defaultPlayersPerDraw); +// const { t } = useIntl(); - return ( - - update({ defaultPlayersPerDraw: next })} - /> - - ); -} +// return ( +// +// update({ defaultPlayersPerDraw: next })} +// /> +// +// ); +// } diff --git a/src/main-view.tsx b/src/main-view.tsx index 0183dc83f..e78c1fa36 100644 --- a/src/main-view.tsx +++ b/src/main-view.tsx @@ -3,7 +3,6 @@ import { PlayerNamesControls } from "./controls/player-names"; import { DrawingList } from "./drawing-list"; import { atom, useAtom } from "jotai"; import styles from "./main-view.css"; -import { MatchListAndSettings } from "./matches"; export type MainTabId = "drawings" | "players" | "sets"; export const mainTabAtom = atom("drawings"); @@ -22,10 +21,7 @@ export function MainView() { Drawings }> - Participants - - }> - Sets + Start.gg Sync ); diff --git a/src/matches.tsx b/src/matches.tsx index f0957d4ad..e5c9eeff2 100644 --- a/src/matches.tsx +++ b/src/matches.tsx @@ -1,32 +1,10 @@ -import { Button, Card, Classes, Label, Spinner, Text } from "@blueprintjs/core"; -import { useAtom } from "jotai"; -import { ReactNode, useCallback, useRef } from "react"; -import { - startggKeyAtom, - startggEventSlug, - getEventSets, - useStartggMatches, -} from "./startgg-gql"; -import { AppState, useAppDispatch, useAppState } from "./state/store"; -import { setsSlice, Slot, TournamentSet } from "./state/matches.slice"; -import { entrantsSlice } from "./state/entrants.slice"; +import { Button, Card, Classes, Spinner, Text } from "@blueprintjs/core"; +import { useStartggMatches } from "./startgg-gql"; +import { useAppState } from "./state/store"; import { inferShortname } from "./controls/player-names"; import { Refresh } from "@blueprintjs/icons"; import { drawingsSlice } from "./state/drawings.slice"; -export function MatchListAndSettings() { - const sets = useAppState(setsSlice.selectors.selectAll); - return sets.length ? ( - <> - {sets.map((set) => ( - - ))} - - ) : ( - - ); -} - export interface PickedMatch { title: string; players: Array<{ id: string; name: string }>; @@ -38,17 +16,26 @@ export function MatchPicker(props: { onPickMatch?(match: PickedMatch): void }) { const existingMatches = useAppState( drawingsSlice.selectors.associatedMatchIds, ); - const matches = resp.data?.event?.sets?.nodes; + const event = resp.data?.event; + const matches = event?.sets?.nodes; const reloadButton = (
); } - -function TournamentSetPreview(props: { set: TournamentSet }) { - const [slot1, slot2] = props.set.slots; - const getPlayerForSlot = (slot: Slot) => (s: AppState) => { - if (slot.type === "player") - return entrantsSlice.selectors.selectById(s, slot.playerId); - const prereqSet = setsSlice.selectors.selectById(s, slot.setId); - if (prereqSet.winningPlayerId) { - return entrantsSlice.selectors.selectById(s, prereqSet.winningPlayerId); - } - return null; - }; - const player1 = useAppState(getPlayerForSlot(slot1)); - const player2 = useAppState(getPlayerForSlot(slot2)); - return ( - - {props.set.roundText} -{" "} - {player1?.startggTag || TBD} vs{" "} - {player2?.startggTag || TBD} - - ); -} - -export function StartggApiKeyGated(props: { children: ReactNode }) { - const [apiKey, setApiKey] = useAtom(startggKeyAtom); - const [eventSlug, setEventSlug] = useAtom(startggEventSlug); - const apikeyRef = useRef(null); - const slugRef = useRef(null); - const saveKey = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!apikeyRef.current || !slugRef.current) return; - setApiKey(apikeyRef.current.value); - setEventSlug(slugRef.current.value); - }, - [setApiKey, setEventSlug], - ); - if (apiKey && eventSlug) { - return props.children; - } - return ( -
- Start.gg info needed - - - -
- ); -} - -function StartggSetImport() { - const [apiKey, setApiKey] = useAtom(startggKeyAtom); - const [eventSlug, setEventSlug] = useAtom(startggEventSlug); - const dispatch = useAppDispatch(); - const importEntrants = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!apiKey || !eventSlug) { - return; - } - const sets = await getEventSets(eventSlug); - dispatch(setsSlice.actions.upsertMany(sets)); - }, - [dispatch, apiKey, eventSlug], - ); - return ( -
- No sets added yet, import from start.gg - - - -
- ); -} diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index 7ba477f0c..bf138799d 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -80,7 +80,7 @@ export interface Drawing { meta: SimpleMeta | StartggMeta; /** index of items of the players array, in the order they should be displayed */ playerDisplayOrder: number[]; - /** map of song ID to player index or id */ + /** map of song ID to player index */ winners: Record; charts: Array; bans: Record; diff --git a/src/startgg-gql/components.tsx b/src/startgg-gql/components.tsx new file mode 100644 index 000000000..f6136bbc1 --- /dev/null +++ b/src/startgg-gql/components.tsx @@ -0,0 +1,65 @@ +import { Classes, Label, Text } from "@blueprintjs/core"; +import { useAtomValue, useAtom } from "jotai"; +import { ReactNode, useRef, useCallback } from "react"; +import { startggKeyAtom, startggEventSlug } from "."; + +export function StartggApiKeyGated(props: { children: ReactNode }) { + const apiKey = useAtomValue(startggKeyAtom); + const eventSlug = useAtomValue(startggEventSlug); + if (apiKey && eventSlug) { + return props.children; + } else { + return ; + } +} + +export function StartggCredsManager() { + const [apiKey, setApiKey] = useAtom(startggKeyAtom); + const [eventSlug, setEventSlug] = useAtom(startggEventSlug); + const apikeyRef = useRef(null); + const slugRef = useRef(null); + const saveKey = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + if (!apikeyRef.current || !slugRef.current) return; + setApiKey(apikeyRef.current.value); + setEventSlug(slugRef.current.value); + }, + [setApiKey, setEventSlug], + ); + return ( +
+ + Start.gg credentials are saved locally on this device and never synced + with other devices + + + + +
+ ); +} diff --git a/src/startgg-gql/generated/gql.ts b/src/startgg-gql/generated/gql.ts index 5d92f15a1..0991c0eb0 100644 --- a/src/startgg-gql/generated/gql.ts +++ b/src/startgg-gql/generated/gql.ts @@ -17,6 +17,7 @@ const documents = { "\n query SetName($sid: ID!) {\n set(id: $sid) {\n __typename\n id\n fullRoundText\n }\n }\n": types.SetNameDocument, "\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n": types.EventEntrantsDocument, "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n": types.EventSetsDocument, + "\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n": types.ReportSetDocument, }; /** @@ -49,6 +50,10 @@ export function graphql(source: "\n query EventEntrants($eventSlug: String!, $p * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"): (typeof documents)["\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/startgg-gql/generated/graphql.ts b/src/startgg-gql/generated/graphql.ts index 278aecba5..5dc55663b 100644 --- a/src/startgg-gql/generated/graphql.ts +++ b/src/startgg-gql/generated/graphql.ts @@ -2432,8 +2432,18 @@ export type EventSetsQueryVariables = Exact<{ export type EventSetsQuery = { __typename?: 'Query', event?: { __typename?: 'Event', id?: string | null, sets?: { __typename?: 'SetConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null, total?: number | null } | null, nodes?: Array<{ __typename?: 'Set', id?: string | null, fullRoundText?: string | null, identifier?: string | null, slots?: Array<{ __typename?: 'SetSlot', prereqType?: string | null, prereqId?: string | null, prereqPlacement?: number | null, entrant?: { __typename?: 'Entrant', id?: string | null, name?: string | null } | null } | null> | null } | null> | null } | null } | null }; +export type ReportSetMutationVariables = Exact<{ + setId: Scalars['ID']['input']; + winnerId?: InputMaybe; + gameData?: InputMaybe> | InputMaybe>; +}>; + + +export type ReportSetMutation = { __typename?: 'Mutation', reportBracketSet?: Array<{ __typename?: 'Set', id?: string | null, completedAt?: any | null } | null> | null }; + export const PlayerNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlayerName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const SetNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SetName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"set"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}}]}}]}}]} as unknown as DocumentNode; export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"entrants"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const EventSetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventSets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hideEmpty"},"value":{"kind":"BooleanValue","value":true}}]}},{"kind":"Argument","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"slots"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prereqType"}},{"kind":"Field","name":{"kind":"Name","value":"prereqId"}},{"kind":"Field","name":{"kind":"Name","value":"prereqPlacement"}},{"kind":"Field","name":{"kind":"Name","value":"entrant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const EventSetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventSets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hideEmpty"},"value":{"kind":"BooleanValue","value":true}}]}},{"kind":"Argument","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"slots"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prereqType"}},{"kind":"Field","name":{"kind":"Name","value":"prereqId"}},{"kind":"Field","name":{"kind":"Name","value":"prereqPlacement"}},{"kind":"Field","name":{"kind":"Name","value":"entrant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const ReportSetDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReportSet"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"setId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BracketSetGameDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reportBracketSet"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"setId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"setId"}}},{"kind":"Argument","name":{"kind":"Name","value":"winnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"gameData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completedAt"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 52c05be45..3cf924e16 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -1,10 +1,8 @@ -import { useQuery } from "urql"; -import { Entrant } from "../state/entrants.slice"; -import { TournamentSet } from "../state/matches.slice"; +import { useMutation, useQuery } from "urql"; import { - EventEntrantsDocument, EventSetsDocument, PlayerNameDocument, + ReportSetDocument, SetNameDocument, } from "./generated/graphql"; import { Client, fetchExchange, gql } from "@urql/core"; @@ -86,62 +84,33 @@ export function useStartggMatches() { }); } -const EventEntrantsDoc: typeof EventEntrantsDocument = gql` - query EventEntrants($eventSlug: String!, $pageNo: Int!) { - event(slug: $eventSlug) { - __typename - id - name - entrants(query: { page: $pageNo, perPage: 100 }) { - pageInfo { - totalPages - } - nodes { - __typename - id - name - # paginatedSets { - # nodes { - # id - # } - # pageInfo { - # totalPages - # } - # } - } - } - } - } -`; - -export async function getEventEntrants(slug: string) { - let pageNo = 0; - - const ret: Entrant[] = []; - - let totalPages = 0; - do { - const results = await urqlClient - .query(EventEntrantsDoc, { - eventSlug: slug, - pageNo, - }) - .toPromise(); - totalPages = results.data?.event?.entrants?.pageInfo?.totalPages || 0; - if (results.data?.event?.entrants?.nodes) { - for (const entrant of results.data.event.entrants.nodes) { - if (!entrant) continue; - ret.push({ - id: entrant.id!, - startggTag: entrant.name!, - }); - } - } - pageNo++; - } while (totalPages > pageNo + 1); - - return ret; -} +// const EventEntrantsDoc: typeof EventEntrantsDocument = gql` +// query EventEntrants($eventSlug: String!, $pageNo: Int!) { +// event(slug: $eventSlug) { +// __typename +// id +// name +// entrants(query: { page: $pageNo, perPage: 100 }) { +// pageInfo { +// totalPages +// } +// nodes { +// __typename +// id +// name +// # paginatedSets { +// # nodes { +// # id +// # } +// # pageInfo { +// # totalPages +// # } +// # } +// } +// } +// } +// } +// `; const EventSetsDoc: typeof EventSetsDocument = gql` query EventSets($eventSlug: String!, $pageNo: Int!) { @@ -171,36 +140,28 @@ const EventSetsDoc: typeof EventSetsDocument = gql` } `; -export async function getEventSets(slug: string) { - let pageNo = 0; - - const ret: TournamentSet[] = []; - - let totalPages = 0; - do { - const results = await urqlClient - .query(EventSetsDoc, { - eventSlug: slug, - pageNo, - }) - .toPromise(); - totalPages = results.data?.event?.sets?.pageInfo?.totalPages || 0; - if (results.data?.event?.sets?.nodes) { - for (const set of results.data.event.sets.nodes) { - if (!set) continue; - ret.push({ - id: set.id!, - roundText: set.fullRoundText!, - slots: set.slots!.map((slot) => - slot!.entrant?.id - ? { type: "player", playerId: slot!.entrant.id } - : { type: "setprereq", setId: slot!.prereqId! }, - ), - }); - } +const ReportSetMutation: typeof ReportSetDocument = gql` + mutation ReportSet( + $setId: ID! + $winnerId: ID + $gameData: [BracketSetGameDataInput] + ) { + reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) { + id + completedAt } - pageNo++; - } while (totalPages > pageNo + 1); + } +`; + +export type { + BracketSetGameDataInput, + ReportSetMutationVariables, +} from "./generated/graphql"; - return ret; +/** + * Passing a winnerId will mark the set as completed. + * Passing game data will overwrite any existing game data. + */ +export function useReportSetMutation() { + return useMutation(ReportSetMutation); } diff --git a/src/state/entrants.slice.ts b/src/state/entrants.slice.ts deleted file mode 100644 index e4fc69700..000000000 --- a/src/state/entrants.slice.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; -export interface Entrant { - id: string; - ingameName?: string; - startggTag: string; -} - -const entrantsAdapter = createEntityAdapter(); -const selectors = entrantsAdapter.getSelectors(); - -export const entrantsSlice = createSlice({ - name: "entrants", - initialState: entrantsAdapter.getInitialState(), - reducers: { - removeMany: entrantsAdapter.removeMany, - upsertMany: entrantsAdapter.upsertMany, - upsertOne: entrantsAdapter.updateOne, - }, - selectors: { - selectAll: selectors.selectAll, - selectById: selectors.selectById, - selectFromIds(state, entrantIds: string[]) { - return entrantIds.map((id) => selectors.selectById(state, id)); - }, - selectIds: selectors.selectIds, - }, -}); diff --git a/src/state/matches.slice.ts b/src/state/matches.slice.ts deleted file mode 100644 index d372b5098..000000000 --- a/src/state/matches.slice.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createEntityAdapter, createSlice } from "@reduxjs/toolkit"; - -interface SetPlayer { - type: "player"; - playerId: string; -} - -interface SetPrereq { - type: "setprereq"; - setId: string; -} - -export type Slot = SetPlayer | SetPrereq; - -export interface TournamentSet { - id: string; - roundText: string; - slots: Slot[]; - winningPlayerId?: string; -} - -const setsAdapter = createEntityAdapter(); -const selectors = setsAdapter.getSelectors(); - -export const setsSlice = createSlice({ - name: "sets", - initialState: setsAdapter.getInitialState(), - reducers: { - removeMany: setsAdapter.removeMany, - upsertMany: setsAdapter.upsertMany, - upsertOne: setsAdapter.updateOne, - }, - selectors: { - selectAll: selectors.selectAll, - selectById: selectors.selectById, - selectIds: selectors.selectIds, - }, -}); diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index 65e9f37fd..d0ffe07a6 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -4,16 +4,12 @@ import { drawingsSlice } from "./drawings.slice"; import { gameDataSlice } from "./game-data.slice"; import { receivePartyState } from "./central"; import { eventSlice } from "./event.slice"; -import { entrantsSlice } from "./entrants.slice"; -import { setsSlice } from "./matches.slice"; const combinedReducer = combineSlices( drawingsSlice, configSlice, gameDataSlice, eventSlice, - entrantsSlice, - setsSlice, ); export const reducer: typeof combinedReducer = (state, action) => { diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index 5971be41d..46d7bf67e 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -1,19 +1,116 @@ import { Button, Menu, MenuItem, Popover, Tooltip } from "@blueprintjs/core"; -import { Camera, Refresh, Error, CubeAdd, Exchange } from "@blueprintjs/icons"; +import { + Camera, + Refresh, + Error, + CubeAdd, + Exchange, + FloppyDisk, +} from "@blueprintjs/icons"; import { useDrawing } from "../drawing-context"; import styles from "./drawing-actions.css"; import { domToPng } from "modern-screenshot"; import { shareImage } from "../utils/share"; import { useErrorBoundary } from "react-error-boundary"; -import { useAppDispatch, useAppState } from "../state/store"; +import { AppThunk, useAppDispatch, useAppState } from "../state/store"; import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; import { drawingsSlice } from "../state/drawings.slice"; import { eventSlice } from "../state/event.slice"; import { createRedrawAll } from "../state/thunks"; +import { + useReportSetMutation, + ReportSetMutationVariables as MutationVariables, + BracketSetGameDataInput as GDI, +} from "../startgg-gql"; +import { CountingSet } from "../utils/counting-set"; + +/** thunk that dispatches nothing, but calculates the result to be sent to startgg */ +function getMatchResult( + drawingId: string, +): AppThunk { + return (_, getState): MutationVariables | undefined => { + const s = getState(); + const gameData: Array = []; + const drawing = s.drawings.entities[drawingId]; + if (drawing.meta.type !== "startgg") { + return; + } + const winsPerPlayer = new CountingSet(); + for (const [songId, pIdx] of Object.entries(drawing.winners)) { + if (pIdx === null) { + continue; + } + try { + const entrant = drawing.meta.entrants[pIdx]; + gameData.push({ + gameNum: gameData.length + 1, + winnerId: entrant.id, + }); + winsPerPlayer.add(entrant.id); + } catch (e) { + console.warn(`failed to add game data for song ${songId}`, e); + } + } + let winnerId: string | undefined; + const orderedByWins = Array.from(winsPerPlayer.valuesWithCount()).sort( + (a, b) => b[1] - a[1], + ); + if (orderedByWins[0][1] > orderedByWins[1][1]) { + // confirmed no tie for first place + winnerId = orderedByWins[0][0]; + } + const ret: MutationVariables = { + setId: drawing.meta.id, + winnerId, + }; + if (gameData.length) { + ret.gameData = gameData; + } + return ret; + }; +} const DEFAULT_FILENAME = "card-draw.png"; +function SaveToStartggButton() { + const dispatch = useAppDispatch(); + const drawingId = useDrawing((s) => s.id); + const drawingMeta = useDrawing((s) => s.meta); + const [mutationData, reportSet] = useReportSetMutation(); + if (drawingMeta.type !== "startgg") { + return null; + } + + let tooltipContent = "Save Winner to Start.gg"; + if (mutationData.error) { + tooltipContent = `Error saving: ${mutationData.error.message}`; + } + if (mutationData.fetching) { + tooltipContent = "Saving..."; + } + + return ( + + +

- { - handleDraw(match); - setMatchPickerOpen(false); - }} - /> + From 6699a3fbaa140dec72e5cb6627bcbac41720c06c Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sun, 18 Aug 2024 22:38:46 -0700 Subject: [PATCH 053/168] add quick picker for startgg events --- src/controls/player-names.tsx | 1 + src/startgg-gql/components.tsx | 87 +++++++++++++++++++++++----- src/startgg-gql/generated/gql.ts | 8 +-- src/startgg-gql/generated/graphql.ts | 20 +++---- src/startgg-gql/index.ts | 42 ++++++++++++++ 5 files changed, 130 insertions(+), 28 deletions(-) diff --git a/src/controls/player-names.tsx b/src/controls/player-names.tsx index 96e68d8af..a3edacdf0 100644 --- a/src/controls/player-names.tsx +++ b/src/controls/player-names.tsx @@ -16,6 +16,7 @@ export function PlayerNamesControls() { title="Start.gg Credentials" collapsible collapseProps={{ defaultIsOpen: !apiKey || !eventSlug }} + style={{ maxWidth: "50em" }} > diff --git a/src/startgg-gql/components.tsx b/src/startgg-gql/components.tsx index f6136bbc1..7e95530ff 100644 --- a/src/startgg-gql/components.tsx +++ b/src/startgg-gql/components.tsx @@ -1,7 +1,7 @@ -import { Classes, Label, Text } from "@blueprintjs/core"; -import { useAtomValue, useAtom } from "jotai"; -import { ReactNode, useRef, useCallback } from "react"; -import { startggKeyAtom, startggEventSlug } from "."; +import { Button, Classes, InputGroup, Label, Text } from "@blueprintjs/core"; +import { useAtomValue, useAtom, useSetAtom } from "jotai"; +import React, { ReactNode, useRef, useCallback } from "react"; +import { startggKeyAtom, startggEventSlug, useCurrentUserEvents } from "."; export function StartggApiKeyGated(props: { children: ReactNode }) { const apiKey = useAtomValue(startggKeyAtom); @@ -21,8 +21,9 @@ export function StartggCredsManager() { const saveKey = useCallback( async (e: React.FormEvent) => { e.preventDefault(); - if (!apikeyRef.current || !slugRef.current) return; + if (!apikeyRef.current) return; setApiKey(apikeyRef.current.value); + if (!slugRef.current) return; setEventSlug(slugRef.current.value); }, [setApiKey, setEventSlug], @@ -39,11 +40,10 @@ export function StartggCredsManager() { create a personal token here ){" "} - Save} /> - + {!!apiKey && ( + { + if (slugRef.current) { + slugRef.current.value = slug; + } + }} + /> + )} ); } + +function EventPicker(props: { onSelected(slug: string): void }) { + const [result] = useCurrentUserEvents(); + const setEventSlug = useSetAtom(startggEventSlug); + const tournaments = result.data?.currentUser?.tournaments?.nodes; + + function handleSelect(e: React.MouseEvent) { + e.preventDefault(); + const slug = e.currentTarget.dataset.slug; + if (slug) { + setEventSlug(slug); + props.onSelected(slug); + } + } + + if (!tournaments) { + return null; + } + return ( + <> +

Try the easy way and pick from your tournaments:

+
    + {tournaments.map((t) => { + if (!t) return null; + const events = t.events; + return ( +
  • + {t.name} + {events?.length ? ( +
      + {events.map((evt) => { + if (!evt) return null; + return ( +
    • + + {evt.name} + +
    • + ); + })} +
    + ) : ( + " (no events)" + )} +
  • + ); + })} +
+ + ); +} diff --git a/src/startgg-gql/generated/gql.ts b/src/startgg-gql/generated/gql.ts index 0991c0eb0..260bffed2 100644 --- a/src/startgg-gql/generated/gql.ts +++ b/src/startgg-gql/generated/gql.ts @@ -15,9 +15,9 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ const documents = { "\n query PlayerName($pid: ID!) {\n entrant(id: $pid) {\n __typename\n id\n name\n }\n }\n": types.PlayerNameDocument, "\n query SetName($sid: ID!) {\n set(id: $sid) {\n __typename\n id\n fullRoundText\n }\n }\n": types.SetNameDocument, - "\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n": types.EventEntrantsDocument, "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n": types.EventSetsDocument, "\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n": types.ReportSetDocument, + "\n query EventList($page: Int!, $perPage: Int!) {\n currentUser {\n tournaments(\n query: {\n page: $page\n perPage: $perPage\n filter: { tournamentView: \"admin\", upcoming: true }\n }\n ) {\n nodes {\n id\n name\n slug\n events {\n id\n name\n slug\n }\n }\n pageInfo {\n total\n totalPages\n page\n perPage\n }\n }\n }\n }\n": types.EventListDocument, }; /** @@ -45,15 +45,15 @@ export function graphql(source: "\n query SetName($sid: ID!) {\n set(id: $si /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventEntrants($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n __typename\n id\n name\n entrants(query: { page: $pageNo, perPage: 100 }) {\n pageInfo {\n totalPages\n }\n nodes {\n __typename\n id\n name\n # paginatedSets {\n # nodes {\n # id\n # }\n # pageInfo {\n # totalPages\n # }\n # }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query EventSets($eventSlug: String!, $pageNo: Int!) {\n event(slug: $eventSlug) {\n id\n sets(filters: { hideEmpty: true }, perPage: 100, page: $pageNo) {\n pageInfo {\n totalPages\n total\n }\n nodes {\n id\n fullRoundText\n identifier\n slots {\n prereqType\n prereqId\n prereqPlacement\n entrant {\n id\n name\n }\n }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"): (typeof documents)["\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"): (typeof documents)["\n mutation ReportSet(\n $setId: ID!\n $winnerId: ID\n $gameData: [BracketSetGameDataInput]\n ) {\n reportBracketSet(setId: $setId, winnerId: $winnerId, gameData: $gameData) {\n id\n completedAt\n }\n }\n"]; +export function graphql(source: "\n query EventList($page: Int!, $perPage: Int!) {\n currentUser {\n tournaments(\n query: {\n page: $page\n perPage: $perPage\n filter: { tournamentView: \"admin\", upcoming: true }\n }\n ) {\n nodes {\n id\n name\n slug\n events {\n id\n name\n slug\n }\n }\n pageInfo {\n total\n totalPages\n page\n perPage\n }\n }\n }\n }\n"): (typeof documents)["\n query EventList($page: Int!, $perPage: Int!) {\n currentUser {\n tournaments(\n query: {\n page: $page\n perPage: $perPage\n filter: { tournamentView: \"admin\", upcoming: true }\n }\n ) {\n nodes {\n id\n name\n slug\n events {\n id\n name\n slug\n }\n }\n pageInfo {\n total\n totalPages\n page\n perPage\n }\n }\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/startgg-gql/generated/graphql.ts b/src/startgg-gql/generated/graphql.ts index 5dc55663b..2c5b76faa 100644 --- a/src/startgg-gql/generated/graphql.ts +++ b/src/startgg-gql/generated/graphql.ts @@ -2416,14 +2416,6 @@ export type SetNameQueryVariables = Exact<{ export type SetNameQuery = { __typename?: 'Query', set?: { __typename: 'Set', id?: string | null, fullRoundText?: string | null } | null }; -export type EventEntrantsQueryVariables = Exact<{ - eventSlug: Scalars['String']['input']; - pageNo: Scalars['Int']['input']; -}>; - - -export type EventEntrantsQuery = { __typename?: 'Query', event?: { __typename: 'Event', id?: string | null, name?: string | null, entrants?: { __typename?: 'EntrantConnection', pageInfo?: { __typename?: 'PageInfo', totalPages?: number | null } | null, nodes?: Array<{ __typename: 'Entrant', id?: string | null, name?: string | null } | null> | null } | null } | null }; - export type EventSetsQueryVariables = Exact<{ eventSlug: Scalars['String']['input']; pageNo: Scalars['Int']['input']; @@ -2441,9 +2433,17 @@ export type ReportSetMutationVariables = Exact<{ export type ReportSetMutation = { __typename?: 'Mutation', reportBracketSet?: Array<{ __typename?: 'Set', id?: string | null, completedAt?: any | null } | null> | null }; +export type EventListQueryVariables = Exact<{ + page: Scalars['Int']['input']; + perPage: Scalars['Int']['input']; +}>; + + +export type EventListQuery = { __typename?: 'Query', currentUser?: { __typename?: 'User', tournaments?: { __typename?: 'TournamentConnection', nodes?: Array<{ __typename?: 'Tournament', id?: string | null, name?: string | null, slug?: string | null, events?: Array<{ __typename?: 'Event', id?: string | null, name?: string | null, slug?: string | null } | null> | null } | null> | null, pageInfo?: { __typename?: 'PageInfo', total?: number | null, totalPages?: number | null, page?: number | null, perPage?: number | null } | null } | null } | null }; + export const PlayerNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlayerName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const SetNameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SetName"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"set"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}}]}}]}}]} as unknown as DocumentNode; -export const EventEntrantsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventEntrants"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"entrants"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const EventSetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventSets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hideEmpty"},"value":{"kind":"BooleanValue","value":true}}]}},{"kind":"Argument","name":{"kind":"Name","value":"perPage"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageNo"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullRoundText"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"slots"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"prereqType"}},{"kind":"Field","name":{"kind":"Name","value":"prereqId"}},{"kind":"Field","name":{"kind":"Name","value":"prereqPlacement"}},{"kind":"Field","name":{"kind":"Name","value":"entrant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const ReportSetDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReportSet"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"setId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BracketSetGameDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reportBracketSet"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"setId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"setId"}}},{"kind":"Argument","name":{"kind":"Name","value":"winnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"gameData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completedAt"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ReportSetDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReportSet"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"setId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BracketSetGameDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reportBracketSet"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"setId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"setId"}}},{"kind":"Argument","name":{"kind":"Name","value":"winnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"winnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"gameData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"gameData"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completedAt"}}]}}]}}]} as unknown as DocumentNode; +export const EventListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"perPage"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tournaments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"perPage"},"value":{"kind":"Variable","name":{"kind":"Name","value":"perPage"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"tournamentView"},"value":{"kind":"StringValue","value":"admin","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"upcoming"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"totalPages"}},{"kind":"Field","name":{"kind":"Name","value":"page"}},{"kind":"Field","name":{"kind":"Name","value":"perPage"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/startgg-gql/index.ts b/src/startgg-gql/index.ts index 3cf924e16..d3c0c4963 100644 --- a/src/startgg-gql/index.ts +++ b/src/startgg-gql/index.ts @@ -4,6 +4,7 @@ import { PlayerNameDocument, ReportSetDocument, SetNameDocument, + EventListDocument, } from "./generated/graphql"; import { Client, fetchExchange, gql } from "@urql/core"; import { cacheExchange } from "@urql/exchange-graphcache"; @@ -165,3 +166,44 @@ export type { export function useReportSetMutation() { return useMutation(ReportSetMutation); } + +const EventListQuery: typeof EventListDocument = gql` + query EventList($page: Int!, $perPage: Int!) { + currentUser { + tournaments( + query: { + page: $page + perPage: $perPage + filter: { tournamentView: "admin" } + } + ) { + nodes { + id + name + slug + events { + id + name + slug + } + } + pageInfo { + total + totalPages + page + perPage + } + } + } + } +`; + +export function useCurrentUserEvents() { + return useQuery({ + query: EventListQuery, + variables: { + page: 1, + perPage: 25, + }, + }); +} From c41193f1881121efd12f4b3139e8dab34edcfcd8 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 21 Aug 2024 20:51:25 -0700 Subject: [PATCH 054/168] another dedupe --- yarn.lock | 95 +++++-------------------------------------------------- 1 file changed, 8 insertions(+), 87 deletions(-) diff --git a/yarn.lock b/yarn.lock index 03c261368..0eeba3043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,7 +167,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6": +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.24.7": version: 7.24.8 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.8" dependencies: @@ -186,25 +186,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-create-class-features-plugin@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.24.7" - "@babel/helper-environment-visitor": "npm:^7.24.7" - "@babel/helper-function-name": "npm:^7.24.7" - "@babel/helper-member-expression-to-functions": "npm:^7.24.7" - "@babel/helper-optimise-call-expression": "npm:^7.24.7" - "@babel/helper-replace-supers": "npm:^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/8ecb1c2acc808e1e0c21dccc7ea6899de9a140cb1856946800176b4784de6fccd575661fbff7744bb895d01aa6956ce963446b8577c4c2334293ba5579d5cdb9 - languageName: node - linkType: hard - "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.24.7" @@ -1515,7 +1496,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.24.8 resolution: "@babel/runtime@npm:7.24.8" dependencies: @@ -1524,15 +1505,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": - version: 7.24.4 - resolution: "@babel/runtime@npm:7.24.4" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308 - languageName: node - linkType: hard - "@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.24.7": version: 7.24.7 resolution: "@babel/template@npm:7.24.7" @@ -3296,20 +3268,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10/97106439d750a409c22c8bff822d648f6a71f3aa9bc8e5129efdc36343cd3096ddc4eeb1c62d2fe48e9bdd4db37b05d4646a17114ecebd3bbcacfa2de51c3c1d languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.1 - resolution: "@jridgewell/resolve-uri@npm:3.1.1" - checksum: 10/64d59df8ae1a4e74315eb1b61e012f1c7bc8aac47a3a1e683f6fe7008eab07bc512a742b7aa7c0405685d1421206de58c9c2e6adbfe23832f8bd69408ffc183e - languageName: node - linkType: hard - "@jridgewell/set-array@npm:^1.2.1": version: 1.2.1 resolution: "@jridgewell/set-array@npm:1.2.1" @@ -4047,7 +4012,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.0.0": +"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10": version: 8.5.11 resolution: "@types/ws@npm:8.5.11" dependencies: @@ -4056,15 +4021,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.10": - version: 8.5.10 - resolution: "@types/ws@npm:8.5.10" - dependencies: - "@types/node": "npm:*" - checksum: 10/9b414dc5e0b6c6f1ea4b1635b3568c58707357f68076df9e7cd33194747b7d1716d5189c0dbdd68c8d2521b148e88184cf881bac7429eb0e5c989b001539ed31 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -4206,7 +4162,7 @@ __metadata: languageName: node linkType: hard -"@urql/core@npm:^5.0.0": +"@urql/core@npm:^5.0.0, @urql/core@npm:^5.0.4": version: 5.0.5 resolution: "@urql/core@npm:5.0.5" dependencies: @@ -4216,16 +4172,6 @@ __metadata: languageName: node linkType: hard -"@urql/core@npm:^5.0.4": - version: 5.0.4 - resolution: "@urql/core@npm:5.0.4" - dependencies: - "@0no-co/graphql.web": "npm:^1.0.5" - wonka: "npm:^6.3.2" - checksum: 10/fe3ee871bde8ee8931d1f791a1475f8bf97940eb93eaa690b11dbfcfa9409644ea87c204245f64e22ea1ddeb16a55e7532b765c18eedcdd9ae8032981889d06a - languageName: node - linkType: hard - "@urql/exchange-graphcache@npm:^7.1.2": version: 7.1.2 resolution: "@urql/exchange-graphcache@npm:7.1.2" @@ -4544,7 +4490,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.8.0": +"acorn@npm:^8.11.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.0 resolution: "acorn@npm:8.12.0" bin: @@ -4553,15 +4499,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.11.3 - resolution: "acorn@npm:8.11.3" - bin: - acorn: bin/acorn - checksum: 10/b688e7e3c64d9bfb17b596e1b35e4da9d50553713b3b3630cf5690f2b023a84eac90c56851e6912b483fe60e8b4ea28b254c07e92f17ef83d72d78745a8352dd - languageName: node - linkType: hard - "agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": version: 7.1.0 resolution: "agent-base@npm:7.1.0" @@ -9014,7 +8951,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.17.1, jiti@npm:^1.18.2": +"jiti@npm:^1.17.1, jiti@npm:^1.18.2, jiti@npm:^1.20.0": version: 1.21.6 resolution: "jiti@npm:1.21.6" bin: @@ -9023,15 +8960,6 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.20.0": - version: 1.21.0 - resolution: "jiti@npm:1.21.0" - bin: - jiti: bin/jiti.js - checksum: 10/005a0239e50381b5c9919f59dbab86128367bd64872f3376dbbde54b6523f41bd134bf22909e2a509e38fd87e1c22125ca255b9b6b53e7df0fedd23f737334cc - languageName: node - linkType: hard - "jose@npm:^5.0.0": version: 5.6.3 resolution: "jose@npm:5.6.3" @@ -13041,14 +12969,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:~2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca - languageName: node - linkType: hard - -"tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.5.0, tslib@npm:^2.6.1, tslib@npm:~2.6.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.1, tslib@npm:^2.6.2, tslib@npm:~2.6.0, tslib@npm:~2.6.2": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c From a79f4aace7bc16c03712c45db3fe8cb7994c9faa Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 2 Sep 2024 13:47:50 -0700 Subject: [PATCH 055/168] experimental static cards display endpoint --- src/app.tsx | 13 +++++++ src/drawn-set.tsx | 11 ++++++ src/obs-sources/static-cards.tsx | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/obs-sources/static-cards.tsx diff --git a/src/app.tsx b/src/app.tsx index 0a37ca32c..665cda878 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -46,6 +46,19 @@ const router = createBrowserRouter([ ); }, }, + { + path: "static-cards", + lazy: async () => { + const { StaticCards } = await import("./obs-sources/static-cards"); + return { + element: ( + + + + ), + }; + }, + }, { path: "e/:roomName", element: , diff --git a/src/drawn-set.tsx b/src/drawn-set.tsx index 1bed3891e..b9c1cef6d 100644 --- a/src/drawn-set.tsx +++ b/src/drawn-set.tsx @@ -8,6 +8,7 @@ import { DrawingActions } from "./tournament-mode/drawing-actions"; import { ErrorFallback } from "./utils/error-fallback"; import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "./config-state"; +import { EligibleChart } from "./models/Drawing"; const HUE_STEP = (255 / 8) * 3; let hue = Math.floor(Math.random() * 255); @@ -40,6 +41,16 @@ export function ChartsOnly({ drawingId }: Props) { ); } +export function RawChartList(props: { charts: Array }) { + return ( +
+ {props.charts.map((c, idx) => ( + + ))} +
+ ); +} + function ChartFromContext({ chartId }: { chartId: string }) { const chart = useDrawing((d) => d.charts.find((c) => c.id === chartId)); const veto = useDrawing((d) => d.bans[chartId]); diff --git a/src/obs-sources/static-cards.tsx b/src/obs-sources/static-cards.tsx new file mode 100644 index 000000000..e686b5ee0 --- /dev/null +++ b/src/obs-sources/static-cards.tsx @@ -0,0 +1,60 @@ +import { useSearchParams } from "react-router-dom"; +import { availableGameData } from "../utils"; +import { useEffect } from "react"; +import { useAtomValue } from "jotai"; +import { gameDataAtom } from "../state/game-data.atoms"; +import { gameDataSlice } from "../state/game-data.slice"; +import { useAppDispatch } from "../state/store"; +import { GameData } from "../models/SongData"; +import { getDrawnChart } from "../card-draw"; +import { RawChartList } from "../drawn-set"; + +export function StaticCards() { + const [searchParams] = useSearchParams(); + const game = searchParams.get("game"); + if (!game) { + return "must specify game param"; + } + if (!availableGameData.some((data) => data.name === game)) { + return `no available game known by stub ${game}. use one of ${availableGameData.map((data) => data.name).join(", ")}`; + } + const charts = searchParams.getAll("chart"); + if (!charts.length) { + return "must specify charts with the chart param"; + } + return ; +} + +function StaticCardsLoader(props: { game: string; charts: string[] }) { + const gameData = useAtomValue(gameDataAtom); + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch( + gameDataSlice.actions.selectGameData({ + dataSetName: props.game, + dataType: "stock", + }), + ); + }, [dispatch, props.game]); + if (!gameData) { + return null; + } + return ; +} + +function StaticCardsPresentation(props: { + gameData: GameData; + charts: string[]; +}) { + const cardDatas = props.charts + .map((chartString) => { + const [songId, chartId] = chartString.split(","); + const song = props.gameData.songs.find((s) => s.saIndex === songId); + if (!song) return null; + const chart = song.charts[+chartId]; + if (!chart) return null; + return getDrawnChart(props.gameData, song, chart); + }) + .filter((chart) => chart !== null); + return ; +} From 43eb438b505f52554064dbf8d3584b1740af4b4c Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Tue, 3 Sep 2024 23:33:34 -0700 Subject: [PATCH 056/168] early bits of multiconfig (all broken) --- src/card-draw.ts | 1 + src/config-state.ts | 44 +--------- src/controls/controls-drawer.tsx | 10 ++- src/controls/degrs-tester.tsx | 4 +- src/drawing-context.tsx | 1 + src/models/Drawing.ts | 1 + src/state/config.slice.ts | 138 +++++++++++++++++++++++++------ src/state/game-data.atoms.ts | 7 -- src/state/hooks.ts | 34 -------- src/state/hooks.tsx | 61 ++++++++++++++ src/state/listener-middleware.ts | 6 +- src/state/store.ts | 5 +- src/state/thunks.ts | 15 ++-- 13 files changed, 203 insertions(+), 124 deletions(-) delete mode 100644 src/state/hooks.ts create mode 100644 src/state/hooks.tsx diff --git a/src/card-draw.ts b/src/card-draw.ts index 5a4efaefd..f1035fa6e 100644 --- a/src/card-draw.ts +++ b/src/card-draw.ts @@ -439,6 +439,7 @@ export function draw( return { id: `draw-${nanoid(10)}`, + configId: configData.id, bans: {}, protects: {}, pocketPicks: {}, diff --git a/src/config-state.ts b/src/config-state.ts index 3581f1c8a..10e1561f5 100644 --- a/src/config-state.ts +++ b/src/config-state.ts @@ -3,46 +3,4 @@ import { atom } from "jotai"; export const showEligibleCharts = atom(false); export const showPlayerAndRoundLabels = atom(true); -export interface ConfigState { - chartCount: number; - playerPicks: number; - upperBound: number; - lowerBound: number; - useWeights: boolean; - orderByAction: boolean; - hideVetos: boolean; - weights: Array; - probabilityBucketCount: number | null; - forceDistribution: boolean; - constrainPocketPicks: boolean; - style: string; - folders: Array; - difficulties: Array; - flags: Array; - cutoffDate: string; - defaultPlayersPerDraw: number; - sortByLevel: boolean; - useGranularLevels: boolean; -} - -export const initialState: ConfigState = { - chartCount: 5, - playerPicks: 0, - upperBound: 0, - lowerBound: 0, - useWeights: false, - hideVetos: false, - orderByAction: true, - weights: [], - probabilityBucketCount: null, - forceDistribution: true, - constrainPocketPicks: true, - style: "", - cutoffDate: "", - folders: [], - difficulties: [], - flags: [], - sortByLevel: false, - defaultPlayersPerDraw: 2, - useGranularLevels: false, -}; +export type { ConfigState } from "./state/config.slice"; diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index 03f2ab727..b9242d282 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -30,7 +30,11 @@ import { getAvailableLevels } from "../game-data-utils"; import { ShowChartsToggle } from "./show-charts-toggle"; import { Fraction } from "../utils/fraction"; import { detectedLanguage } from "../utils"; -import { useConfigState, useUpdateConfig } from "../state/hooks"; +import { + SelectedConfigContextProvider, + useConfigState, + useUpdateConfig, +} from "../state/hooks"; import { useAtomValue } from "jotai"; import { showEligibleCharts } from "../config-state"; import { useAppState } from "../state/store"; @@ -76,7 +80,9 @@ function getDiffsAndRangeForNewStyle( export default function ControlsDrawer() { return (
- + + +
); } diff --git a/src/controls/degrs-tester.tsx b/src/controls/degrs-tester.tsx index 82ce55f23..5d86e8536 100644 --- a/src/controls/degrs-tester.tsx +++ b/src/controls/degrs-tester.tsx @@ -29,11 +29,11 @@ export function isDegrs(thing: EligibleChart | PlayerPickPlaceholder) { } function* oneMillionDraws(gameData: GameData) { - const configState = configSlice.selectSlice(store.getState()); + const configState = configSlice.selectors.getCurrent(store.getState()); for (let idx = 0; idx < TEST_SIZE; idx++) { yield [ - draw(gameData, configState, { + draw(gameData, configState!, { meta: { players: [], title: "", type: "simple" }, }), idx, diff --git a/src/drawing-context.tsx b/src/drawing-context.tsx index dcfb10ea7..ee6a6f5bf 100644 --- a/src/drawing-context.tsx +++ b/src/drawing-context.tsx @@ -6,6 +6,7 @@ import { EqualityFn } from "react-redux"; const stubDrawing: Drawing = { id: "", + configId: "", meta: { type: "simple", players: [], diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index bf138799d..31032b52b 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -77,6 +77,7 @@ export function playerNameByIndex( export interface Drawing { id: string; + configId: string; meta: SimpleMeta | StartggMeta; /** index of items of the players array, in the order they should be displayed */ playerDisplayOrder: number[]; diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index a79c80c4d..d69a9ad32 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -1,36 +1,122 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { ConfigState, initialState } from "../config-state"; +import { + createSlice, + createEntityAdapter, + PayloadAction, +} from "@reduxjs/toolkit"; import { GameData } from "../models/SongData"; +import { nanoid } from "nanoid"; + +export interface ConfigState { + id: string; + name: string; + gameKey: string; + chartCount: number; + playerPicks: number; + upperBound: number; + lowerBound: number; + useWeights: boolean; + orderByAction: boolean; + hideVetos: boolean; + weights: Array; + probabilityBucketCount: number | null; + forceDistribution: boolean; + constrainPocketPicks: boolean; + style: string; + folders: Array; + difficulties: Array; + flags: Array; + cutoffDate: string; + defaultPlayersPerDraw: number; + sortByLevel: boolean; + useGranularLevels: boolean; +} + +export const defaultConfig: Omit = { + chartCount: 5, + playerPicks: 0, + upperBound: 0, + lowerBound: 0, + useWeights: false, + hideVetos: false, + orderByAction: true, + weights: [], + probabilityBucketCount: null, + forceDistribution: true, + constrainPocketPicks: true, + style: "", + cutoffDate: "", + folders: [], + difficulties: [], + flags: [], + sortByLevel: false, + defaultPlayersPerDraw: 2, + useGranularLevels: false, +}; + +const adapter = createEntityAdapter({}); export const configSlice = createSlice({ name: "config", - initialState, + initialState: { + ...adapter.getInitialState(), + current: null as string | null, + }, reducers: { - update(state, action: PayloadAction>) { - Object.assign(state, action.payload); + pickCurrent(state, action: PayloadAction) { + state.current = action.payload; }, - applyDefaults( - state, - action: PayloadAction< - GameData["defaults"] & { supportsGranular: boolean } - >, - ) { - const { flags, difficulties, folders, style } = action.payload; - const patch: Partial = { - lowerBound: action.payload.lowerLvlBound, - upperBound: action.payload.upperLvlBound, - flags, - difficulties, - style, - cutoffDate: "", - }; - if (folders) { - patch.folders = folders; - } - if (!action.payload.supportsGranular) { - patch.useGranularLevels = false; + addOne: adapter.addOne, + updateOne: adapter.updateOne, + removeOne: adapter.removeOne, + }, + selectors: { + ...adapter.getSelectors(), + getCurrent(state) { + if (state.current) { + return state.entities[state.current]; } - Object.assign(state, patch); + return null; }, }, }); + +function getOverridesFromGameData(gameData: GameData) { + const { + flags, + difficulties, + folders, + style, + lowerLvlBound: lowerBound, + upperLvlBound: upperBound, + } = gameData.defaults; + const gameSpecificOverrides: Partial = { + lowerBound, + upperBound, + flags, + difficulties, + style, + cutoffDate: "", + }; + if (folders) { + gameSpecificOverrides.folders = folders; + } + if (!gameData.meta.granularTierResolution) { + gameSpecificOverrides.useGranularLevels = false; + } + return gameSpecificOverrides; +} + +export function createFromGameData( + gameData: GameData, + name: string, + gameKey: string, +) { + const newConfig: ConfigState = { + id: nanoid(10), + name, + gameKey, + ...defaultConfig, + ...getOverridesFromGameData(gameData), + }; + return configSlice.actions.addOne(newConfig); +} diff --git a/src/state/game-data.atoms.ts b/src/state/game-data.atoms.ts index 8a543c1f7..e2f208694 100644 --- a/src/state/game-data.atoms.ts +++ b/src/state/game-data.atoms.ts @@ -1,7 +1,6 @@ import { atom, getDefaultStore } from "jotai"; import { GameData } from "../models/SongData"; import { startAppListening } from "./listener-middleware"; -import { configSlice } from "./config.slice"; export const gameDataAtom = atom(null); @@ -53,12 +52,6 @@ startAppListening({ if (newData) { console.log("data arrived", { newData }); jotaiStore.set(gameDataAtom, newData); - api.dispatch( - configSlice.actions.applyDefaults({ - supportsGranular: !!newData.meta.granularTierResolution, - ...newData.defaults, - }), - ); jotaiStore.set(gameDataLoadingStatus, "available"); } else { console.log("data did not arrive"); diff --git a/src/state/hooks.ts b/src/state/hooks.ts deleted file mode 100644 index e15d142bf..000000000 --- a/src/state/hooks.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { store, useAppDispatch, useAppState } from "./store"; -import { EqualityFn } from "react-redux"; -import { useCallback } from "react"; -import { ConfigState } from "../config-state"; -import { configSlice } from "./config.slice"; - -export function useConfigState( - selector?: (state: ConfigState) => T, - equalityFn?: EqualityFn, -) { - return useAppState((state) => { - const sliceState = configSlice.selectSlice(state); - if (!selector) return sliceState as T; - return selector(sliceState); - }, equalityFn); -} - -export function useUpdateConfig() { - const dispatch = useAppDispatch(); - return useCallback( - ( - patch: - | Partial - | ((state: ConfigState) => Partial), - ) => { - if (typeof patch === "function") { - const state = configSlice.selectSlice(store.getState()); - patch = patch(state); - } - dispatch(configSlice.actions.update(patch)); - }, - [dispatch], - ); -} diff --git a/src/state/hooks.tsx b/src/state/hooks.tsx new file mode 100644 index 000000000..b53eecf76 --- /dev/null +++ b/src/state/hooks.tsx @@ -0,0 +1,61 @@ +import { useAppDispatch, useAppState } from "./store"; +import { EqualityFn } from "react-redux"; +import { createContext, ReactNode, useCallback, useContext } from "react"; +import { ConfigState } from "../config-state"; +import { configSlice } from "./config.slice"; + +const configContext = createContext(null); + +export const ConfigContextProvider = configContext.Provider; +export function SelectedConfigContextProvider(props: { children: ReactNode }) { + const selected = useAppState(configSlice.selectors.getCurrent); + if (!selected) return null; + return ( + + {props.children} + + ); +} + +function useConfigId() { + const id = useContext(configContext); + if (!id) { + throw new Error("config id used without provider parent"); + } + return id; +} + +export function useConfigState( + selector?: (state: ConfigState) => T, + equalityFn?: EqualityFn, +) { + const configId = useConfigId(); + return useAppState((state) => { + const configObj = configSlice.selectors.selectById(state, configId); + if (!selector) return configObj as T; + return selector(configObj); + }, equalityFn); +} + +export function useUpdateConfig() { + const configId = useConfigId(); + const dispatch = useAppDispatch(); + return useCallback( + ( + patch: + | Partial + | ((state: ConfigState) => Partial), + ) => { + dispatch((dispatch, getState) => { + if (typeof patch === "function") { + const state = configSlice.selectors.selectById(getState(), configId); + patch = patch(state); + } + dispatch( + configSlice.actions.updateOne({ id: configId, changes: patch }), + ); + }); + }, + [dispatch, configId], + ); +} diff --git a/src/state/listener-middleware.ts b/src/state/listener-middleware.ts index 9184fa02c..c1612ae41 100644 --- a/src/state/listener-middleware.ts +++ b/src/state/listener-middleware.ts @@ -1,9 +1,11 @@ import { createListenerMiddleware } from "@reduxjs/toolkit"; import type { AppState, AppDispatch } from "./store"; -export const listenerMiddleware = createListenerMiddleware(); +const listener = createListenerMiddleware(); -export const startAppListening = listenerMiddleware.startListening.withTypes< +export const middleware = listener.middleware; + +export const startAppListening = listener.startListening.withTypes< AppState, AppDispatch >(); diff --git a/src/state/store.ts b/src/state/store.ts index f74555f20..0d594f05b 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -5,12 +5,11 @@ import { } from "@reduxjs/toolkit"; import { useDispatch, useSelector, useStore } from "react-redux"; import { reducer } from "./root-reducer"; -import { listenerMiddleware } from "./listener-middleware"; +import { middleware as listener } from "./listener-middleware"; export const store = configureStore({ reducer, - middleware: (getDefaults) => - getDefaults().concat(listenerMiddleware.middleware), + middleware: (getDefaults) => getDefaults().concat(listener), }); export type AppState = ReturnType; diff --git a/src/state/thunks.ts b/src/state/thunks.ts index d1fcc001c..fff03aee4 100644 --- a/src/state/thunks.ts +++ b/src/state/thunks.ts @@ -4,6 +4,7 @@ import { getDefaultStore } from "jotai"; import { gameDataAtom } from "./game-data.atoms"; import { drawingsSlice } from "./drawings.slice"; import { EligibleChart } from "../models/Drawing"; +import { configSlice } from "./config.slice"; declare const umami: { track( @@ -30,12 +31,13 @@ export function createDraw(startggTargetSet: StartggInfo): AppThunk { const state = getState(); const jotaiStore = getDefaultStore(); const gameData = jotaiStore.get(gameDataAtom); - if (!gameData) { + const config = configSlice.selectors.getCurrent(state); + if (!gameData || !config) { trackDraw(null); return false; // no draw was possible } - const drawing = draw(gameData, state.config, startggTargetSet); + const drawing = draw(gameData, config, startggTargetSet); trackDraw(drawing.charts.length, state.gameData.dataSetName); if (!drawing.charts.length) { return false; // could not draw the requested number of charts @@ -52,8 +54,9 @@ export function createRedrawAll(drawingId: string): AppThunk { return (dispatch, getState) => { const state = getState(); const drawing = state.drawings.entities[drawingId]; + const originalConfig = state.config.entities[drawing.configId]; const drawConfig = { - ...state.config, + ...originalConfig, chartCount: drawing.charts.length, }; const jotaiStore = getDefaultStore(); @@ -102,12 +105,13 @@ export function createRedrawChart( if (!gameData) return; const state = getState(); const drawing = state.drawings.entities[drawingId]; + const originalConfig = state.config.entities[drawing.configId]; const startingPoint = { ...drawing, charts: drawing.charts.filter((chart) => chart.id !== chartId), }; - const drawResult = draw(gameData, state.config, startingPoint); + const drawResult = draw(gameData, originalConfig, startingPoint); const chart = drawResult.charts.pop(); if ( !chart || @@ -135,7 +139,8 @@ export function createPickBanPocket( pick?: EligibleChart, ): AppThunk { return (dispatch, getState) => { - const reorder = getState().config.orderByAction; + const reorder = + !!configSlice.selectors.getCurrent(getState())?.orderByAction; let action; if (type === "pocket") { if (pick) { From 7dff004f1c22628d58859d9be27d3c3d8cb40662 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 6 Sep 2024 01:31:22 -0700 Subject: [PATCH 057/168] kind of working multiconfig --- src/controls/controls-drawer.tsx | 124 ++++++++++++++++++++++++++++++- src/controls/index.tsx | 74 +++++++++++++----- src/drawn-set.tsx | 57 +++++++------- src/header.tsx | 4 +- src/matches.tsx | 16 ++-- src/startgg-gql/index.ts | 1 + src/state/config.slice.ts | 8 +- src/state/drawings.slice.ts | 7 -- src/state/game-data.atoms.ts | 36 ++++++++- src/state/hooks.tsx | 3 +- src/state/store.ts | 2 + src/state/thunks.ts | 27 +++---- 12 files changed, 274 insertions(+), 85 deletions(-) diff --git a/src/controls/controls-drawer.tsx b/src/controls/controls-drawer.tsx index b9242d282..a67967f15 100644 --- a/src/controls/controls-drawer.tsx +++ b/src/controls/controls-drawer.tsx @@ -3,11 +3,17 @@ import { ButtonGroup, Card, Checkbox, + Classes, Collapse, + ControlGroup, + Dialog, + DialogBody, + DialogFooter, Divider, FormGroup, HTMLSelect, NumericInput, + Tooltip, } from "@blueprintjs/core"; import { CaretDown, @@ -15,6 +21,9 @@ import { Plus, SmallTick, SmallCross, + Add, + Duplicate, + Delete, } from "@blueprintjs/icons"; import { DateInput3 } from "@blueprintjs/datetime2"; import parse from "date-fns/parse"; @@ -29,7 +38,7 @@ import styles from "./controls.css"; import { getAvailableLevels } from "../game-data-utils"; import { ShowChartsToggle } from "./show-charts-toggle"; import { Fraction } from "../utils/fraction"; -import { detectedLanguage } from "../utils"; +import { availableGameData, detectedLanguage } from "../utils"; import { SelectedConfigContextProvider, useConfigState, @@ -37,8 +46,9 @@ import { } from "../state/hooks"; import { useAtomValue } from "jotai"; import { showEligibleCharts } from "../config-state"; -import { useAppState } from "../state/store"; -import { gameDataAtom } from "../state/game-data.atoms"; +import { createAppSelector, useAppDispatch, useAppState } from "../state/store"; +import { gameDataAtom, useStockGameData } from "../state/game-data.atoms"; +import { configSlice, createConfigFromInputs } from "../state/config.slice"; function getAvailableDifficulties(gameData: GameData, selectedStyle: string) { const s = new Set(); @@ -80,6 +90,7 @@ function getDiffsAndRangeForNewStyle( export default function ControlsDrawer() { return (
+ @@ -87,6 +98,111 @@ export default function ControlsDrawer() { ); } +const getConfigEntries = createAppSelector( + [(s) => s.config.entities], + (entities) => + Object.entries(entities).map( + ([key, config]) => + [ + key, + config.name, + config.gameKey, + config.lowerBound, + config.upperBound, + ] as const, + ), +); + +function ControlsList() { + const summaryValues = useAppState(getConfigEntries); + const selected = useAppState((s) => s.config.current); + const [addOpen, setAddOpen] = useState(false); + const [busyCreating, setBusyCreating] = useState(false); + const dispatch = useAppDispatch(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (busyCreating) return; + + const data = new FormData(e.currentTarget); + const name = data.get("name") as string; + const gameStub = data.get("game") as string; + + if (!name) { + return; + } + if (!gameStub) { + return; + } + + setBusyCreating(true); + dispatch(await createConfigFromInputs(name, gameStub)); + setAddOpen(false); + setBusyCreating(false); + }; + + return ( +
+ setAddOpen(false)} + title="Create Config" + > +
+ + + + + + + {availableGameData.map((data) => ( + + ))} + + + + + Create + + } + > +
+
+ + + + dispatch(configSlice.actions.pickCurrent(e.currentTarget.value)) + } + > + {summaryValues.map(([key, name, gameKey, lb, ub]) => ( + + ))} + + +
+ ); +} + const dateFormat = "yyyy-MM-dd"; function ReleaseDateFilter() { const { t } = useIntl(); @@ -231,10 +347,10 @@ function FolderSettings() { function GeneralSettings() { const { t } = useIntl(); - const gameData = useAtomValue(gameDataAtom); const updateState = useUpdateConfig(); const showingEligibleCharts = useAtomValue(showEligibleCharts); const configState = useConfigState(); + const gameData = useStockGameData(configState.gameKey); const { useWeights, constrainPocketPicks, diff --git a/src/controls/index.tsx b/src/controls/index.tsx index 3a3887647..a263f2205 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -1,10 +1,11 @@ import { Button, - ButtonGroup, + ControlGroup, Dialog, DialogBody, Drawer, DrawerSize, + HTMLSelect, Intent, NavbarDivider, Position, @@ -19,21 +20,58 @@ import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "../utils/error-fallback"; import { ShowChartsToggle } from "./show-charts-toggle"; import { createDraw } from "../state/thunks"; -import { useAppDispatch } from "../state/store"; -import { useAtomValue, useSetAtom } from "jotai"; +import { createAppSelector, useAppDispatch, useAppState } from "../state/store"; +import { useSetAtom } from "jotai"; import { showEligibleCharts } from "../config-state"; -import { gameDataLoadingStatus } from "../state/game-data.atoms"; import { MatchPicker, PickedMatch } from "../matches"; import { StartggApiKeyGated } from "../startgg-gql/components"; +import { configSlice } from "../state/config.slice"; const ControlsDrawer = lazy(() => import("./controls-drawer")); +const getConfigEntries = createAppSelector( + [(s) => s.config.entities], + (entities) => + Object.entries(entities).map(([key, config]) => [key, config.name]), +); + +function ConfigSelect() { + const configEntries = useAppState(getConfigEntries); + const current = useAppState((s) => s.config.current); + const dispatch = useAppDispatch(); + + if (!configEntries.length) { + return ( + + + + ); + } + + return ( + + dispatch(configSlice.actions.pickCurrent(e.currentTarget.value)) + } + > + {configEntries.map(([key, name]) => ( + + ))} + + ); +} + export function HeaderControls() { const setShowEligibleCharts = useSetAtom(showEligibleCharts); const [settingsOpen, setSettingsOpen] = useState(false); const [lastDrawFailed, setLastDrawFailed] = useState(false); const [matchPickerOpen, setMatchPickerOpen] = useState(false); - const hasGameData = useAtomValue(gameDataLoadingStatus) === "available"; + const hasConfig = useAppState((s) => !!s.config.current); const isNarrow = useIsNarrow(); const dispatch = useAppDispatch(); @@ -118,17 +156,8 @@ export function HeaderControls() { )} - - - - + + } @@ -142,7 +171,18 @@ export function HeaderControls() { data-umami-event="settings-open" /> - + + + + + ); } diff --git a/src/drawn-set.tsx b/src/drawn-set.tsx index b9c1cef6d..b94e4c80d 100644 --- a/src/drawn-set.tsx +++ b/src/drawn-set.tsx @@ -9,6 +9,8 @@ import { ErrorFallback } from "./utils/error-fallback"; import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "./config-state"; import { EligibleChart } from "./models/Drawing"; +import { ConfigContextProvider } from "./state/hooks"; +import { useAppState } from "./state/store"; const HUE_STEP = (255 / 8) * 3; let hue = Math.floor(Math.random() * 255); @@ -83,39 +85,42 @@ function TournamentModeSpacer() { const DrawnSet = memo(function DrawnSet({ drawingId }) { const [backgroundImage] = useState(getRandomGradiant()); + const configId = useAppState((s) => s.drawings.entities[drawingId].configId); return ( - + + +
+ } + >
- -
- } - > -
- -
- - + +
+ + +
+
- -
- + + ); }); diff --git a/src/header.tsx b/src/header.tsx index b1d420e55..ed7d2921a 100644 --- a/src/header.tsx +++ b/src/header.tsx @@ -14,7 +14,6 @@ import { HeaderControls } from "./controls"; import { useIntl } from "./hooks/useIntl"; import { LastUpdate } from "./last-update"; import { ThemeToggle } from "./theme-toggle"; -import { DataLoadingSpinner, VersionSelect } from "./version-select"; import { useAppDispatch, useAppState } from "./state/store"; import { drawingsSlice } from "./state/drawings.slice"; @@ -68,8 +67,7 @@ export function Header() { - } - > - - - - - - dispatch(configSlice.actions.pickCurrent(e.currentTarget.value)) - } - > - {summaryValues.map(([key, name, gameKey, lb, ub]) => ( - - ))} - - - - - */} + } > @@ -186,3 +197,161 @@ export function HeaderControls() { ); } + +const getConfigSummaryValues = createAppSelector( + [(s) => s.config.entities], + (entities) => + Object.entries(entities).map( + ([key, config]) => + [ + key, + config.name, + config.gameKey, + config.lowerBound, + config.upperBound, + ] as const, + ), +); + +function ControlsList() { + const summaryValues = useAppState(getConfigSummaryValues); + const selected = useAppState((s) => s.config.current); + const selectedName = useAppState((s) => + selected ? s.config.entities[selected].name : undefined, + ); + const selectedGameId = useAppState((s) => + selected ? s.config.entities[selected].gameKey : undefined, + ); + const [addOpen, setAddOpen] = useState(false); + const [busyCreating, setBusyCreating] = useState(false); + const dispatch = useAppDispatch(); + const createBasisRef = useRef(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (busyCreating) return; + + const data = new FormData(e.currentTarget); + const name = data.get("name") as string; + const gameStub = data.get("game") as string; + + if (!name) { + return; + } + if (!gameStub) { + return; + } + + setBusyCreating(true); + await dispatch( + createConfigFromInputs(name, gameStub, createBasisRef.current), + ); + setAddOpen(false); + setBusyCreating(false); + createBasisRef.current = undefined; + }; + + return ( + <> + { + setAddOpen(false); + createBasisRef.current = undefined; + }} + title={"Create Config"} + > +
+ + + + + + + + + + Create + + } + > +
+
+ + } + onClick={() => setAddOpen(true)} + /> + } + onClick={() => { + createBasisRef.current = selected || undefined; + setAddOpen(true); + }} + /> + } + onClick={() => dispatch(configSlice.actions.removeOne(selected!))} + /> + + } + onClick={loadConfig} + /> + } + onClick={() => + saveConfig( + dispatch( + (d, gs) => gs().config.entities[gs().config.current!], + ), + ) + } + /> +
+ } + > + + + ); +} + export function HeaderControls() { const setShowEligibleCharts = useSetAtom(showEligibleCharts); const [settingsOpen, setSettingsOpen] = useState(false); @@ -97,23 +139,17 @@ export function HeaderControls() { const isNarrow = useIsNarrow(); const dispatch = useAppDispatch(); - function handleDraw(match?: PickedMatch) { + function handleDraw(match: PickedMatch) { setMatchPickerOpen(false); setShowEligibleCharts(false); const result = dispatch( createDraw({ - meta: match - ? { - type: "startgg", - entrants: match.players, - title: match.title, - id: match.id, - } - : { - type: "simple", - title: "", - players: ["", ""], - }, + meta: { + type: "startgg", + entrants: match.players, + title: match.title, + id: match.id, + }, }), ); if (typeof result === "boolean") { @@ -154,15 +190,28 @@ export function HeaderControls() { title="New Draw" > -

- Pick a startgg match or{" "} - -

- - - + + + + + } + > + start.gg match + + dispatch(createDraw({ meta }))} + /> + } + > + custom draw + +
{!isNarrow && ( diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index 31032b52b..d7896b2b4 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -54,6 +54,23 @@ export interface SimpleMeta { players: string[]; } +export function playerCount(meta: Drawing["meta"]) { + switch (meta.type) { + case "simple": + return meta.players.length; + case "startgg": + return meta.entrants.length; + } +} + +export function getAllPlayers(d: Pick) { + const ret = [] as string[]; + for (let i = 1; i <= playerCount(d.meta); i++) { + ret.push(playerNameByDisplayPos(d, i)); + } + return ret; +} + export function playerNameByDisplayPos( d: Pick, pos: number, diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index c9046ca2d..bcecf9f2b 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -25,6 +25,7 @@ import { BracketSetGameDataInput as GDI, } from "../startgg-gql"; import { CountingSet } from "../utils/counting-set"; +import { playerCount } from "../models/Drawing"; /** thunk that dispatches nothing, but calculates the result to be sent to startgg */ function getMatchResult( @@ -135,6 +136,7 @@ export function DrawingActions() { const dispatch = useAppDispatch(); const cabs = useAppState(eventSlice.selectors.allCabs); const drawingId = useDrawing((s) => s.id); + const isTwoPlayers = useDrawing((s) => playerCount(s.meta) === 2); const showLabels = useAtomValue(showPlayerAndRoundLabels); const { showBoundary } = useErrorBoundary(); @@ -201,7 +203,7 @@ export function DrawingActions() { )} - {showLabels && ( + {showLabels && isTwoPlayers && ( @@ -131,27 +153,32 @@ function CustomDrawForm(props: { } export function HeaderControls() { - const setShowEligibleCharts = useSetAtom(showEligibleCharts); + const [configId, setConfigId] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [lastDrawFailed, setLastDrawFailed] = useState(false); const [matchPickerOpen, setMatchPickerOpen] = useState(false); - const hasConfig = useAppState((s) => !!s.config.current); + const hasAnyConfig = useAppState((s) => !!s.config.ids.length); const isNarrow = useIsNarrow(); const dispatch = useAppDispatch(); function handleDraw(match: PickedMatch) { + if (!configId) { + return; + } setMatchPickerOpen(false); - setShowEligibleCharts(false); const result = dispatch( - createDraw({ - meta: { - type: "startgg", - subtype: match.subtype, - entrants: match.players, - title: match.title, - id: match.id, + createDraw( + { + meta: { + type: "startgg", + subtype: match.subtype, + entrants: match.players, + title: match.title, + id: match.id, + }, }, - }), + configId, + ), ); if (typeof result === "boolean") { setLastDrawFailed(result); @@ -167,30 +194,24 @@ export function HeaderControls() { return ( <> - setSettingsOpen(false)} - title={ - <> - - - - } - > - }> - }> - - - - + /> setMatchPickerOpen(false)} title="New Draw" > + + + dispatch(createDraw({ meta }))} + disableCreate={!configId} + onSubmit={(meta) => dispatch(createDraw({ meta }, configId!))} /> } > @@ -225,14 +247,7 @@ export function HeaderControls() { - {!isNarrow && ( - <> - - - - )} - - + } @@ -241,45 +256,64 @@ export function HeaderControls() { position={Position.BOTTOM_RIGHT} > - - - - + icon={} + onClick={openSettings} + data-umami-event="settings-open" + /> + ); } -const getConfigSummaryValues = createAppSelector( - [(s) => s.config.entities], - (entities) => - Object.entries(entities).map( - ([key, config]) => - [ - key, - config.name, - config.gameKey, - config.lowerBound, - config.upperBound, - ] as const, - ), -); +function MultiControlsDrawer(props: { + isOpen: boolean; + isNarrow: boolean; + onClose(): void; +}) { + const [configId, setConfigId] = useState(null); -function ControlsList() { - const summaryValues = useAppState(getConfigSummaryValues); - const selected = useAppState((s) => s.config.current); + return ( + + + + + } + > + }> + }> + + + + + ); +} + +/** + * provides UI for selecting/creating/cloning/import/exporting configs at the top of the config drawer + */ +function MultiControlsManager(props: { + configId: string | null; + onConfigSelected(configId: string): void; +}) { + const selected = props.configId; const selectedName = useAppState((s) => selected ? s.config.entities[selected].name : undefined, ); @@ -311,10 +345,11 @@ function ControlsList() { typeof createBasisRef.current === "object" ? createConfigFromImport(name, gameStub, createBasisRef.current) : createConfigFromInputs(name, gameStub, createBasisRef.current); - await dispatch(action); + const newConfig = await dispatch(action); setAddOpen(false); setBusyCreating(false); createBasisRef.current = undefined; + props.onConfigSelected(newConfig.id); }; function titleFromBasis() { @@ -411,11 +446,7 @@ function ControlsList() { disabled={!selected} icon={} onClick={() => - saveConfig( - dispatch( - (d, gs) => gs().config.entities[gs().config.current!], - ), - ) + saveConfig(dispatch((d, gs) => gs().config.entities[selected!])) } /> @@ -423,25 +454,11 @@ function ControlsList() { > + + + + + ); + } + return (
- + + + + +
{cabs.map((cab) => ( @@ -43,7 +66,7 @@ export function CabManagement() { ); } -function AddCabControl() { +function AddCabControl(props: { children?: ReactNode }) { const [name, setName] = useState(""); const dispatch = useAppDispatch(); const addCab = useCallback(() => { @@ -64,6 +87,7 @@ function AddCabControl() { placeholder="Cab name" />
@@ -123,22 +123,19 @@ function AppForRoom() { - - -
-
- - -
- +
+
+ + +
@@ -147,9 +144,10 @@ function AppForRoom() { export function App() { return ( - <> + + - + ); } From 5f835d79fcb11440595fa01ef45d84a4e7247bda Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 25 Oct 2024 11:03:35 -0700 Subject: [PATCH 087/168] safely return nothing when drawing is missing --- src/obs-sources/text.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/obs-sources/text.tsx b/src/obs-sources/text.tsx index 259148b8c..96824c9bd 100644 --- a/src/obs-sources/text.tsx +++ b/src/obs-sources/text.tsx @@ -8,7 +8,9 @@ export function CabTitle() { const text = useAppState((s) => { const drawingId = s.event.cabs[params.cabId!].activeMatch; if (!drawingId) return null; - return drawingSelectors.selectById(s, drawingId).meta.title; + const drawing = drawingSelectors.selectById(s, drawingId); + if (!drawing) return null; + return drawing.meta.title; }); return

{text}

; } @@ -19,6 +21,7 @@ export function CabPlayers() { const drawingId = s.event.cabs[params.cabId!].activeMatch; if (!drawingId) return null; const drawing = drawingSelectors.selectById(s, drawingId); + if (!drawing) return null; return getAllPlayers(drawing).join(", "); }); return

{text}

; @@ -30,6 +33,7 @@ export function CabPlayer(props: { p: number }) { const drawingId = s.event.cabs[params.cabId!].activeMatch; if (!drawingId) return null; const drawing = drawingSelectors.selectById(s, drawingId); + if (!drawing) return null; const playerIndex = drawing.playerDisplayOrder[props.p - 1]; const name = playerNameByIndex(drawing.meta, playerIndex, ""); const hideWins = From a66db55242c65c7c8c0a506f3822f4a5edbafffb Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 6 Jan 2025 11:02:08 -0800 Subject: [PATCH 088/168] remove artist blocklist --- src/card-draw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/card-draw.ts b/src/card-draw.ts index 8c63070ac..3de5b2bf4 100644 --- a/src/card-draw.ts +++ b/src/card-draw.ts @@ -228,7 +228,7 @@ function bucketIndexForLvl(lvl: number, buckets: LvlRanges): number | null { export type StartggInfo = Pick; export type StartingPoint = Drawing | StartggInfo; -const artistDrawBlocklist = new Set(["Carlito", "Dr. Bombay"]); +const artistDrawBlocklist = new Set(); /** * Produces a drawn set of charts given the song data and the user From 2bbcb3e9844e208f62ccf4dd8eea9bdfba618ae0 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 11 Jan 2025 13:23:05 -0800 Subject: [PATCH 089/168] catch errors in restoring state --- src/party/server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/party/server.ts b/src/party/server.ts index aa1b493e0..7d4036d39 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -29,8 +29,11 @@ export default class Server implements Party.Server { } async onStart() { - const preloadedState = - (await this.getFromStorage()) || (await this.getFromSupabase()); + let preloadedState: AppState | undefined; + try { + preloadedState = + (await this.getFromStorage()) || (await this.getFromSupabase()); + } catch {} if (preloadedState) { this.store = configureStore({ reducer, preloadedState }); } else { From 153c9440e6ca3b493f735b9469847b2a3a53c50f Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 11 Jan 2025 13:34:00 -0800 Subject: [PATCH 090/168] catch errors from upsert, respond to get reqs --- src/party/server.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/party/server.ts b/src/party/server.ts index 7d4036d39..3936f256f 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -5,7 +5,7 @@ import { reducer } from "../state/root-reducer"; import { AppState, type store as appReduxStore } from "../state/store"; import { createClient } from "@supabase/supabase-js"; -import { Database, Json } from "./database.types"; +import type { Database, Json } from "./database.types"; const supabase = createClient( process.env.SUPABASE_URL as string, @@ -57,6 +57,14 @@ export default class Server implements Party.Server { return this.room.storage.get("currentState"); } + onRequest(req: Party.Request): Response | Promise { + if (req.method === "GET") { + return new Response(this.getRoomState()); + } + + return new Response("Method not allowed", { status: 405 }); + } + onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { // A websocket just connected! console.log( @@ -70,7 +78,7 @@ export default class Server implements Party.Server { conn.send(this.getRoomState()); } - onMessage(message: string, sender: Party.Connection) { + async onMessage(message: string, sender: Party.Connection) { // broadcast it to all the other connections in the room... this.room.broadcast( message, @@ -85,11 +93,15 @@ export default class Server implements Party.Server { // persist to partykit storage this.room.storage.put("currentState", nextState); // persist the state to supabase - supabase.from("event_state").upsert({ - id: this.room.id, - state: nextState as unknown as Json, - updated_at: new Date().toISOString(), - }); + try { + await supabase.from("event_state").upsert({ + id: this.room.id, + state: nextState as unknown as Json, + updated_at: new Date().toISOString(), + }); + } catch (e) { + console.warn("error with upsert", e); + } } private getRoomState() { From 822e5b9154a5a515c1746b3223ff0c65d033421c Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Mon, 20 Jan 2025 15:41:50 -0800 Subject: [PATCH 091/168] Add classic mode route with localstorage persistence --- src/app.tsx | 62 ++++---- src/classic-mode/index.tsx | 18 +++ src/classic-mode/localstorage-manager.tsx | 20 +++ src/common-components/app-mode.tsx | 26 ++++ src/controls/draw-dialog.tsx | 143 +++++++++++++++++++ src/controls/index.tsx | 128 +---------------- src/eligible-charts/index.tsx | 2 +- src/header.tsx | 7 +- src/state/localstorage.ts | 14 ++ src/state/root-reducer.ts | 2 + src/state/store.ts | 6 +- src/state/thunks.ts | 9 +- src/{ => tournament-mode}/cab-management.tsx | 10 +- src/tournament-mode/drawing-actions.tsx | 21 +-- src/tournament-mode/index.tsx | 37 +++++ src/{ => tournament-mode}/main-view.css | 0 src/{ => tournament-mode}/main-view.tsx | 10 +- 17 files changed, 331 insertions(+), 184 deletions(-) create mode 100644 src/classic-mode/index.tsx create mode 100644 src/classic-mode/localstorage-manager.tsx create mode 100644 src/common-components/app-mode.tsx create mode 100644 src/controls/draw-dialog.tsx create mode 100644 src/state/localstorage.ts rename src/{ => tournament-mode}/cab-management.tsx (95%) create mode 100644 src/tournament-mode/index.tsx rename src/{ => tournament-mode}/main-view.css (100%) rename src/{ => tournament-mode}/main-view.tsx (78%) diff --git a/src/app.tsx b/src/app.tsx index b3a1e3297..ae8aef0c9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -11,12 +11,10 @@ FocusStyleManager.onlyShowFocusOnTabs(); import { UpdateManager } from "./update-manager"; import { IntlProvider } from "./intl-provider"; -import { Header } from "./header"; import { ThemeSyncWidget } from "./theme-toggle"; import { Provider } from "react-redux"; import { store } from "./state/store"; import { PartySocketManager } from "./party/client"; -import { Provider as UrqlProvider } from "urql"; import { createBrowserRouter, @@ -25,10 +23,8 @@ import { useParams, Link, } from "react-router-dom"; -import { CabManagement } from "./cab-management"; -import { MainView } from "./main-view"; import { nanoid } from "nanoid"; -import { urqlClient } from "./startgg-gql"; +import { ClassicModeShell } from "./classic-mode"; const router = createBrowserRouter([ { @@ -42,6 +38,10 @@ const router = createBrowserRouter([ You need to pick an event first. Would you like to:{" "} Create New Event?

+

+ Or... perhaps just use the app in{" "} + Classic Mode +

No idea what this is?{" "} Here's a video trying to @@ -51,9 +51,31 @@ const router = createBrowserRouter([ ); }, }, + { + path: "classic", + Component: ClassicModeShell, + children: [ + { + index: true, + lazy: async () => { + const mod = await import("./drawing-list"); + return { Component: mod.DrawingList }; + }, + }, + { + path: "charts", + lazy: async () => { + const mod = await import("./eligible-charts"); + return { Component: mod.default }; + }, + }, + ], + }, { path: "e/:roomName", - element: , + lazy: async () => ({ + Component: (await import("./tournament-mode")).TournamentModeApp, + }), }, { path: "e/:roomName/cab/:cabId/source", @@ -114,34 +136,6 @@ function ObsSource() { ); } -function AppForRoom() { - const params = useParams<"roomName">(); - if (!params.roomName) { - return null; - } - return ( - - - -

-
- - -
- - - - ); -} - export function App() { return ( diff --git a/src/classic-mode/index.tsx b/src/classic-mode/index.tsx new file mode 100644 index 000000000..af9aee8e8 --- /dev/null +++ b/src/classic-mode/index.tsx @@ -0,0 +1,18 @@ +import { Provider as ReduxProvider } from "react-redux"; +import { Header } from "../header"; +import { store } from "../state/store"; +import { Outlet } from "react-router-dom"; +import { ClassicModeContext } from "../common-components/app-mode"; +import { LocalStorageManager } from "./localstorage-manager"; + +export function ClassicModeShell() { + return ( + + + +
+ + + + ); +} diff --git a/src/classic-mode/localstorage-manager.tsx b/src/classic-mode/localstorage-manager.tsx new file mode 100644 index 000000000..a1e5ccd9a --- /dev/null +++ b/src/classic-mode/localstorage-manager.tsx @@ -0,0 +1,20 @@ +import { LOCAL_STATE_STORAGE_KEY } from "../state/localstorage"; +import { useAppStore } from "../state/store"; +import { useEffect } from "react"; + +/** + * component that persists the redux state to localstorage with every change + */ +export function LocalStorageManager() { + const store = useAppStore(); + useEffect(() => { + return store.subscribe(() => { + const state = store.getState(); + setTimeout(() => { + const encoded = JSON.stringify(state); + localStorage.setItem(LOCAL_STATE_STORAGE_KEY, encoded); + }); + }); + }, [store]); + return null; +} diff --git a/src/common-components/app-mode.tsx b/src/common-components/app-mode.tsx new file mode 100644 index 000000000..ffba0ea65 --- /dev/null +++ b/src/common-components/app-mode.tsx @@ -0,0 +1,26 @@ +import { createContext, ReactNode, useContext } from "react"; + +const appModeContext = createContext<"classic" | "event">("event"); + +export function ClassicModeContext(props: { children: ReactNode }) { + return ( + + {props.children} + + ); +} + +export function useAppMode() { + return useContext(appModeContext); +} + +export function EventModeGated(props: { + children: ReactNode; + fallback?: ReactNode; +}) { + const mode = useAppMode(); + if (mode === "classic") { + return props.fallback || null; + } + return props.children; +} diff --git a/src/controls/draw-dialog.tsx b/src/controls/draw-dialog.tsx new file mode 100644 index 000000000..8e1aa1827 --- /dev/null +++ b/src/controls/draw-dialog.tsx @@ -0,0 +1,143 @@ +import { + Dialog, + DialogBody, + FormGroup, + Tabs, + Tab, + InputGroup, + TagInput, + Button, +} from "@blueprintjs/core"; +import { ConfigSelect } from "."; +import { MatchPicker, GauntletPicker, PickedMatch } from "../matches"; +import { StartggApiKeyGated } from "../startgg-gql/components"; +import { createDraw } from "../state/thunks"; +import { useAppDispatch } from "../state/store"; +import { SimpleMeta } from "../models/Drawing"; +import { useState } from "react"; +import { EventModeGated } from "../common-components/app-mode"; + +interface Props { + isOpen: boolean; + onClose(): void; + onDrawAttempt(wasSuccess: boolean): void; +} + +export function DrawDialog(props: Props) { + const [configId, setConfigId] = useState(null); + const dispatch = useAppDispatch(); + + function handleDraw(match: PickedMatch) { + if (!configId) { + return; + } + props.onClose(); + dispatch( + createDraw( + { + meta: { + type: "startgg", + subtype: match.subtype, + entrants: match.players, + title: match.title, + id: match.id, + }, + }, + configId, + ), + ).then((result) => { + props.onDrawAttempt(result === "ok"); + }); + } + + return ( + + + + + + + dispatch(createDraw({ meta }, configId!))} + /> + } + > + custom draw + + + + + + } + > + start.gg (h2h) + + + + + } + > + start.gg (gauntlet) + + + + + + ); +} + +function CustomDrawForm(props: { + initialMeta?: SimpleMeta; + disableCreate?: boolean; + onSubmit(meta: SimpleMeta): void; +}) { + const [players, setPlayers] = useState( + props.initialMeta?.players || [], + ); + const [title, setTitle] = useState(props.initialMeta?.title || ""); + + function handleSubmit() { + props.onSubmit({ + type: "simple", + players, + title, + }); + } + return ( + <> + + setTitle(e.currentTarget.value)} + /> + + + setPlayers(v as string[])} + values={players} + /> + + + + ); +} diff --git a/src/controls/index.tsx b/src/controls/index.tsx index 5b6f17bdb..24454dbc5 100644 --- a/src/controls/index.tsx +++ b/src/controls/index.tsx @@ -9,7 +9,6 @@ import { DrawerSize, FormGroup, HTMLSelect, - InputGroup, Intent, Menu, MenuDivider, @@ -17,9 +16,6 @@ import { Popover, Position, Spinner, - Tab, - Tabs, - TagInput, Tooltip, } from "@blueprintjs/core"; import { @@ -40,15 +36,12 @@ import { ErrorFallback } from "../utils/error-fallback"; import { createConfigFromImport, createConfigFromInputs, - createDraw, } from "../state/thunks"; import { createAppSelector, useAppDispatch, useAppState } from "../state/store"; -import { GauntletPicker, MatchPicker, PickedMatch } from "../matches"; -import { StartggApiKeyGated } from "../startgg-gql/components"; import { configSlice, ConfigState } from "../state/config.slice"; import { GameDataSelect } from "../version-select"; import { loadConfig, saveConfig } from "../config-persistence"; -import { SimpleMeta } from "../models/Drawing"; +import { DrawDialog } from "./draw-dialog"; const ControlsDrawer = lazy(() => import("./controls-drawer")); @@ -110,82 +103,12 @@ export function ConfigSelect(props: { ); } -function CustomDrawForm(props: { - initialMeta?: SimpleMeta; - disableCreate?: boolean; - onSubmit(meta: SimpleMeta): void; -}) { - const [players, setPlayers] = useState( - props.initialMeta?.players || [], - ); - const [title, setTitle] = useState(props.initialMeta?.title || ""); - - function handleSubmit() { - props.onSubmit({ - type: "simple", - players, - title, - }); - } - return ( - <> - - setTitle(e.currentTarget.value)} - /> - - - setPlayers(v as string[])} - values={players} - /> - - - - ); -} - export function HeaderControls() { - const [configId, setConfigId] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [lastDrawFailed, setLastDrawFailed] = useState(false); const [matchPickerOpen, setMatchPickerOpen] = useState(false); const hasAnyConfig = useAppState((s) => !!s.config.ids.length); const isNarrow = useIsNarrow(); - const dispatch = useAppDispatch(); - - function handleDraw(match: PickedMatch) { - if (!configId) { - return; - } - setMatchPickerOpen(false); - const result = dispatch( - createDraw( - { - meta: { - type: "startgg", - subtype: match.subtype, - entrants: match.players, - title: match.title, - id: match.id, - }, - }, - configId, - ), - ); - if (typeof result === "boolean") { - setLastDrawFailed(result); - } else { - setLastDrawFailed(false); - } - } function openSettings() { setSettingsOpen((open) => !open); @@ -199,54 +122,11 @@ export function HeaderControls() { isOpen={settingsOpen} onClose={() => setSettingsOpen(false)} /> - setMatchPickerOpen(false)} - title="New Draw" - > - - - - - - dispatch(createDraw({ meta }, configId!))} - /> - } - > - custom draw - - - - - } - > - start.gg (h2h) - - - - - } - > - start.gg (gauntlet) - - - - + onDrawAttempt={(success) => setLastDrawFailed(!success)} + /> Event Mode{" "} - Alpha Preview + + + Alpha Preview + + diff --git a/src/state/localstorage.ts b/src/state/localstorage.ts new file mode 100644 index 000000000..585b4233a --- /dev/null +++ b/src/state/localstorage.ts @@ -0,0 +1,14 @@ +import type { AppState } from "./root-reducer"; + +export const LOCAL_STATE_STORAGE_KEY = "ddrtools.classic.state"; + +export let preloadedState: AppState | undefined; + +try { + const persisted = localStorage.getItem(LOCAL_STATE_STORAGE_KEY); + if (persisted) { + preloadedState = JSON.parse(persisted); + } +} catch { + // YOLO +} diff --git a/src/state/root-reducer.ts b/src/state/root-reducer.ts index ac7505292..d98e58647 100644 --- a/src/state/root-reducer.ts +++ b/src/state/root-reducer.ts @@ -6,6 +6,8 @@ import { eventSlice } from "./event.slice"; const combinedReducer = combineSlices(drawingsSlice, configSlice, eventSlice); +export type AppState = ReturnType; + export const reducer: typeof combinedReducer = (state, action) => { if (receivePartyState.match(action)) { return Object.assign({}, state, action.payload); diff --git a/src/state/store.ts b/src/state/store.ts index 37ab869d8..dc03a5dfa 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -5,15 +5,17 @@ import { createSelector, } from "@reduxjs/toolkit"; import { useDispatch, useSelector, useStore } from "react-redux"; -import { reducer } from "./root-reducer"; +import { reducer, type AppState } from "./root-reducer"; import { middleware as listener } from "./listener-middleware"; +import { preloadedState } from "./localstorage"; export const store = configureStore({ reducer, middleware: (getDefaults) => getDefaults().concat(listener), + preloadedState, }); -export type AppState = ReturnType; +export type { AppState }; export const useAppState = useSelector.withTypes(); export const createAppSelector = createSelector.withTypes(); export type AppDispatch = typeof store.dispatch; diff --git a/src/state/thunks.ts b/src/state/thunks.ts index f3044dc2e..bfebb66db 100644 --- a/src/state/thunks.ts +++ b/src/state/thunks.ts @@ -28,28 +28,29 @@ function trackDraw(count: number | null, game?: string) { export function createDraw( startggTargetSet: StartggInfo, configId: string, -): AppThunk { +): AppThunk> { return async (dispatch, getState) => { const state = getState(); const config = configSlice.selectors.selectById(state, configId); if (!config) { console.error("couldnt draw, no config"); - return false; + return "nok"; } const gameData = await loadStockGamedataByName(config.gameKey); if (!gameData) { console.error("couldnt draw, no game data"); trackDraw(null); - return false; // no draw was possible + return "nok"; // no draw was possible } const drawing = draw(gameData, config, startggTargetSet); trackDraw(drawing.charts.length, gameData.i18n.en.name as string); if (!drawing.charts.length) { - return false; // could not draw the requested number of charts + return "nok"; // could not draw the requested number of charts } dispatch(drawingsSlice.actions.addDrawing(drawing)); + return "ok"; }; } diff --git a/src/cab-management.tsx b/src/tournament-mode/cab-management.tsx similarity index 95% rename from src/cab-management.tsx rename to src/tournament-mode/cab-management.tsx index cb9dd22a2..2d406d043 100644 --- a/src/cab-management.tsx +++ b/src/tournament-mode/cab-management.tsx @@ -8,9 +8,9 @@ import { Popover, Tooltip, } from "@blueprintjs/core"; -import { useAppDispatch, useAppState } from "./state/store"; +import { useAppDispatch, useAppState } from "../state/store"; import React, { ReactNode, useCallback, useState } from "react"; -import { CabInfo, eventSlice } from "./state/event.slice"; +import { CabInfo, eventSlice } from "../state/event.slice"; import { Add, CaretLeft, @@ -24,11 +24,11 @@ import { Person, Remove, } from "@blueprintjs/icons"; -import { detectedLanguage } from "./utils"; -import { copyPlainTextToClipboard } from "./utils/share"; +import { detectedLanguage } from "../utils"; +import { copyPlainTextToClipboard } from "../utils/share"; import { useSetAtom } from "jotai"; import { mainTabAtom } from "./main-view"; -import { playerNameByIndex } from "./models/Drawing"; +import { playerNameByIndex } from "../models/Drawing"; export function CabManagement() { const [isCollapsed, setCollapsed] = useState(true); diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index 634dc5bf8..aec7195e5 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -37,6 +37,7 @@ import { createPlusOneChart, createRedrawAll } from "../state/thunks"; import { CountingSet } from "../utils/counting-set"; import { shareImage } from "../utils/share"; import styles from "./drawing-actions.css"; +import { EventModeGated } from "../common-components/app-mode"; const GauntletEditor = lazy(() => import("./gauntlet-scores")); @@ -215,13 +216,15 @@ export function DrawingActions() { + + ); +} diff --git a/src/preview-mode/shell.tsx b/src/preview-mode/shell.tsx new file mode 100644 index 000000000..602fd32f6 --- /dev/null +++ b/src/preview-mode/shell.tsx @@ -0,0 +1,41 @@ +import { Outlet, useLoaderData } from "react-router-dom"; +import { Provider as ReduxProvider } from "react-redux"; +import { getPartykitState } from "../party/host"; +import { useMemo } from "react"; +import { createClientStore } from "../state/store"; +import { PreviewModeHeader } from "./header"; +import { DrawingList } from "../drawing-list"; +import { ClassicModeContext } from "../common-components/app-mode"; +import { useIntl } from "../hooks/useIntl"; + +async function loader(roomName: string | undefined) { + if (!roomName) { + return; + } + const state = await getPartykitState(roomName); + state.drawings.entities = {}; + state.drawings.ids = []; + return state; +} + +export function PreviewShell() { + const initialState = useLoaderData() as Awaited>; + const store = useMemo(() => createClientStore(initialState), [initialState]); + return ( + + + + ); +} + +PreviewShell.loader = loader; + +export function PreviewView() { + const { t } = useIntl(); + return ( + + + + + ); +} From 000cdd75951d4cc70a915acea429ead7555ab0b5 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 1 Nov 2025 15:17:44 -0700 Subject: [PATCH 143/168] rework config editor to allow generating preview links --- src/controls/config-select.tsx | 147 +++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 45 deletions(-) diff --git a/src/controls/config-select.tsx b/src/controls/config-select.tsx index 41790e514..c0bd4aed7 100644 --- a/src/controls/config-select.tsx +++ b/src/controls/config-select.tsx @@ -3,9 +3,20 @@ import { ButtonGroup, Card, HTMLSelect, + Menu, + MenuItem, + Popover, Tooltip, } from "@blueprintjs/core"; -import { Add, Duplicate, FloppyDisk, Import, Trash } from "@blueprintjs/icons"; +import { + ThAdd, + Duplicate, + FloppyDisk, + Import, + Trash, + More, + DocumentShare, +} from "@blueprintjs/icons"; import { createAppSelector, useAppDispatch, useAppState } from "../state/store"; import styles from "./config-select.css"; import { createNewConfig } from "../state/thunks"; @@ -13,6 +24,8 @@ import { useRoomName } from "../hooks/useRoomName"; import { useSetLastConfigSelected } from "../state/config.atoms"; import { configSlice } from "../state/config.slice"; import { loadConfig, saveConfig } from "../config-persistence"; +import { copyTextToClipboard } from "../utils/share"; +import { useParams } from "react-router-dom"; const getConfigSummaryValues = createAppSelector( [(s) => s.config.entities], @@ -71,6 +84,7 @@ export function ConfigList(props: { const dispatch = useAppDispatch(); const roomName = useRoomName(); const setLastConfigSelected = useSetLastConfigSelected(); + const params = useParams<"roomName">(); const isEmpty = !summaryValues.length; @@ -91,41 +105,39 @@ export function ConfigList(props: { compact > {props.selectedId === key && ( - -
); } + +function ConfigActionsMenu(props: { + onExport(): void; + onShareLink(): void; + onDuplicate(): void; + onDelete(): void; +}) { + const menu = ( + + } + text="Export to JSON" + onClick={props.onExport} + /> + } + text="Share preview link" + onClick={props.onShareLink} + /> + } + text="Duplicate" + onClick={(e) => { + e.preventDefault(); + props.onDuplicate(); + }} + /> + } + text="Delete" + onClick={(e) => { + e.preventDefault(); + props.onDelete(); + }} + /> + + ); + return ( + + + } + /> + + ); +} + +function BatchExportRow(props: { + configId: string; + checked: boolean; + onToggle(id: string): void; +}) { + const config = useAppState((s) => + configSlice.selectors.selectById(s, props.configId), + ); + return ( + props.onToggle(props.configId)} + label={`${config.name} (${config.gameKey}, ${config.lowerBound}-${config.upperBound})`} + /> + ); +} + function ConfigListEntry(props: { configId: string; selected: boolean; @@ -222,6 +365,7 @@ function ConfigActionsMenu(props: { } text="Delete" + intent="danger" onClick={(e) => { e.preventDefault(); props.onDelete(); diff --git a/src/state/config.slice.ts b/src/state/config.slice.ts index fc936d4e9..73096ee93 100644 --- a/src/state/config.slice.ts +++ b/src/state/config.slice.ts @@ -66,6 +66,8 @@ export const configSlice = createSlice({ addOne: adapter.addOne, updateOne: adapter.updateOne, removeOne: adapter.removeOne, + /** insert or fully replace configs, keyed by id */ + setMany: adapter.setMany, // oxlint-enable typescript/unbound-method }, selectors: { From a9acedb5af4dfda2efc77b7f7c08f755741b93c5 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Thu, 11 Jun 2026 12:52:07 -0700 Subject: [PATCH 154/168] bump partysocket, remove peerjs --- package.json | 3 +- yarn.lock | 96 ++++++++-------------------------------------------- 2 files changed, 16 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 63d73eebd..f85512dba 100644 --- a/package.json +++ b/package.json @@ -108,8 +108,7 @@ "p-queue": "9.0.1", "papaparse": "5.5.3", "partykit": "0.0.115", - "partysocket": "1.1.4", - "peerjs": "2.0.0-beta.3", + "partysocket": "1.1.19", "postcss": "8.5.6", "postcss-loader": "8.2.0", "react": "19.2.3", diff --git a/yarn.lock b/yarn.lock index a672428f6..33098c49c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1340,22 +1340,13 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.6": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 languageName: node linkType: hard -"@babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": - version: 7.24.4 - resolution: "@babel/runtime@npm:7.24.4" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308 - languageName: node - linkType: hard - "@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -3248,20 +3239,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10/97106439d750a409c22c8bff822d648f6a71f3aa9bc8e5129efdc36343cd3096ddc4eeb1c62d2fe48e9bdd4db37b05d4646a17114ecebd3bbcacfa2de51c3c1d languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.1 - resolution: "@jridgewell/resolve-uri@npm:3.1.1" - checksum: 10/64d59df8ae1a4e74315eb1b61e012f1c7bc8aac47a3a1e683f6fe7008eab07bc512a742b7aa7c0405685d1421206de58c9c2e6adbfe23832f8bd69408ffc183e - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -4355,7 +4339,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.0.0": +"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10": version: 8.5.11 resolution: "@types/ws@npm:8.5.11" dependencies: @@ -4364,15 +4348,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.10": - version: 8.5.10 - resolution: "@types/ws@npm:8.5.10" - dependencies: - "@types/node": "npm:*" - checksum: 10/9b414dc5e0b6c6f1ea4b1635b3568c58707357f68076df9e7cd33194747b7d1716d5189c0dbdd68c8d2521b148e88184cf881bac7429eb0e5c989b001539ed31 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -6577,8 +6552,7 @@ __metadata: p-queue: "npm:9.0.1" papaparse: "npm:5.5.3" partykit: "npm:0.0.115" - partysocket: "npm:1.1.4" - peerjs: "npm:2.0.0-beta.3" + partysocket: "npm:1.1.19" postcss: "npm:8.5.6" postcss-loader: "npm:8.2.0" react: "npm:19.2.3" @@ -7407,13 +7381,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^3.1.2": - version: 3.1.2 - resolution: "eventemitter3@npm:3.1.2" - checksum: 10/e2886001beb52cd2fe47d2470fd6266b7c70bd3ac356c0041a7e64336ed57bb1fc9b07bc9043d34b39913488a8d81bfcde62d3af597974980aa01b50844d869b - languageName: node - linkType: hard - "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -10995,12 +10962,17 @@ __metadata: languageName: node linkType: hard -"partysocket@npm:1.1.4": - version: 1.1.4 - resolution: "partysocket@npm:1.1.4" +"partysocket@npm:1.1.19": + version: 1.1.19 + resolution: "partysocket@npm:1.1.19" dependencies: event-target-polyfill: "npm:^0.0.4" - checksum: 10/9202439c81db2f240d206e7d18b4a498ddd0269af6c2d200ca1521827bff25af5581bedc66fb9fb3c295c968cdb0ff8b8e9a8c1bf89dfed45669b63755fca9e2 + peerDependencies: + react: ">=17" + peerDependenciesMeta: + react: + optional: true + checksum: 10/e563c9b24ff6e62726f960d2854138db8a87371763085dd2be23a9c42f5887dced188b42d3d8f4c6966da4ae341040595ad7f57141f09101f5c1cc5cff5d79e4 languageName: node linkType: hard @@ -11143,23 +11115,6 @@ __metadata: languageName: node linkType: hard -"peerjs-js-binarypack@npm:1.0.1": - version: 1.0.1 - resolution: "peerjs-js-binarypack@npm:1.0.1" - checksum: 10/a37f2e91f50be3ac651a4a037250c272cdd73bbc82a0ae702f65752907c366403c9a7395a1c25292b7d6a1d43eb945920d21e92f97402cc117d4d5e7cbca9eb0 - languageName: node - linkType: hard - -"peerjs@npm:2.0.0-beta.3": - version: 2.0.0-beta.3 - resolution: "peerjs@npm:2.0.0-beta.3" - dependencies: - eventemitter3: "npm:^3.1.2" - peerjs-js-binarypack: "npm:1.0.1" - checksum: 10/e50b0cebc5bdbec9e31d6f05955d34115b0caaa4ccad31579110a5da2f1df73431a48c2c37a8514393c5ef5ba07b8da65c480999dadd7ce7fc8780ebf60edc7b - languageName: node - linkType: hard - "picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -12154,13 +12109,6 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.14.0": - version: 0.14.1 - resolution: "regenerator-runtime@npm:0.14.1" - checksum: 10/5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 - languageName: node - linkType: hard - "regexp.prototype.flags@npm:^1.5.3": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" @@ -12812,20 +12760,13 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3": +"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": version: 1.8.2 resolution: "shell-quote@npm:1.8.2" checksum: 10/3ae4804fd80a12ba07650d0262804ae3b479a62a6b6971a6dc5fa12995507aa63d3de3e6a8b7a8d18f4ce6eb118b7d75db7fcb2c0acbf016f210f746b10cfe02 languageName: node linkType: hard -"shell-quote@npm:^1.8.1": - version: 1.8.1 - resolution: "shell-quote@npm:1.8.1" - checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d - languageName: node - linkType: hard - "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0" @@ -13677,20 +13618,13 @@ __metadata: languageName: node linkType: hard -"tslib@npm:~2.6.0": +"tslib@npm:~2.6.0, tslib@npm:~2.6.2": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c languageName: node linkType: hard -"tslib@npm:~2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca - languageName: node - linkType: hard - "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" From 93ef61c96fa2668d41e4e600d2a91673684a78a7 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 12 Jun 2026 09:17:49 -0700 Subject: [PATCH 155/168] edit routing to allow players beyond 2 in obs sources --- src/app.tsx | 26 ++++++++-- src/obs-sources/text.tsx | 12 +++-- src/tournament-mode/cab-management.tsx | 72 +++++++++++--------------- src/tournament-mode/copy-obs-source.ts | 1 + 4 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index b15e3e94e..fbaefa1bb 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -174,6 +174,24 @@ const router = createBrowserRouter([ return { Component: CabPlayers }; }, }, + { + path: "player/:playerIndex/:displayType?", + lazy: async () => { + const { CabPlayer, toDisplayType } = + await import("./obs-sources/text"); + return { + Component: function PlayerSource() { + const { playerIndex, displayType } = useParams(); + return ( + + ); + }, + }; + }, + }, { path: "p1", lazy: async () => { @@ -185,14 +203,14 @@ const router = createBrowserRouter([ path: "p1-name", lazy: async () => { const { CabPlayer } = await import("./obs-sources/text"); - return { element: }; + return { element: }; }, }, { path: "p1-score", lazy: async () => { const { CabPlayer } = await import("./obs-sources/text"); - return { element: }; + return { element: }; }, }, { @@ -206,14 +224,14 @@ const router = createBrowserRouter([ path: "p2-name", lazy: async () => { const { CabPlayer } = await import("./obs-sources/text"); - return { element: }; + return { element: }; }, }, { path: "p2-score", lazy: async () => { const { CabPlayer } = await import("./obs-sources/text"); - return { element: }; + return { element: }; }, }, ], diff --git a/src/obs-sources/text.tsx b/src/obs-sources/text.tsx index 5063029ca..efda0903c 100644 --- a/src/obs-sources/text.tsx +++ b/src/obs-sources/text.tsx @@ -38,11 +38,15 @@ export function CabPlayers() { return

{text}

; } +export function toDisplayType(input: string | undefined) { + return input as "name" | "score" | undefined; +} + export function CabPlayer(props: { p: number; - displayType?: "NameAndScore" | "Name" | "Score"; + displayType?: "name" | "score"; }) { - const { displayType = "NameAndScore " } = props; + const { displayType } = props; const params = useParams<"roomName" | "cabId">(); const text = useAppState((s) => { const drawingId = s.event.cabs[params.cabId!].activeMatch; @@ -60,10 +64,10 @@ export function CabPlayer(props: { if (curr === playerIndex) return prev + 1; return prev; }, 0); - if (displayType === "Name") { + if (displayType === "name") { return name; } - if (displayType === "Score") { + if (displayType === "score") { return score; } return `${name} (${score})`; diff --git a/src/tournament-mode/cab-management.tsx b/src/tournament-mode/cab-management.tsx index 02c2047aa..c4798b975 100644 --- a/src/tournament-mode/cab-management.tsx +++ b/src/tournament-mode/cab-management.tsx @@ -25,6 +25,8 @@ import { People, Person, Remove, + Tag, + Numerical, } from "@blueprintjs/icons"; import { detectedLanguage } from "../utils"; import { useSetAtom } from "jotai"; @@ -125,48 +127,34 @@ function CabSummary({ cab }: { cab: CabInfo }) { stub="phase" cabId={cab.id} /> - } - text="All Players" - stub="players" - cabId={cab.id} - /> - } - text="Player 1" - stub="p1" - cabId={cab.id} - /> - } - text="Player 1 Name" - stub="p1-name" - cabId={cab.id} - /> - } - text="Player 1 Score" - stub="p1-score" - cabId={cab.id} - /> - } - text="Player 2" - stub="p2" - cabId={cab.id} - /> - } - text="Player 2 Name" - stub="p2-name" - cabId={cab.id} - /> - } - text="Player 2 Score" - stub="p2-score" - cabId={cab.id} - /> + } text="Players"> + } + text="All Players" + stub="players" + cabId={cab.id} + /> + } text="Single Player"> + + } + text="Name" + stub="player/1/name" + cabId={cab.id} + /> + } + text="Score" + stub="player/1/score" + cabId={cab.id} + /> + + + } text="Remove Cab" onClick={removeCab} /> diff --git a/src/tournament-mode/copy-obs-source.ts b/src/tournament-mode/copy-obs-source.ts index 88cf42bb1..1df7e80c7 100644 --- a/src/tournament-mode/copy-obs-source.ts +++ b/src/tournament-mode/copy-obs-source.ts @@ -2,6 +2,7 @@ import { copyPlainTextToClipboard } from "../utils/share"; export const routableGlobalSourcePath = (labelId: string) => `../obs-globals/${labelId}`; + export const routableCabSourcePath = (cabId: string, sourceName: string) => `cab/${cabId}/source/${sourceName}`; From 731be27606bc67abb5f8e1f20e4822291843b2ef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 05:46:05 +0000 Subject: [PATCH 156/168] Fix removing protect/pocket action also clearing marked winner resetChart deleted the chart's winner along with its bans/protects/ pocketPicks, so removing a protect/pocket label on a card that also had a winner marked would wipe the winner. A chart can legitimately have both an action and a winner at once, and winners have their own removal control, so resetChart no longer deletes the winner. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WLriWZavKpUVqb9VRoB6vL --- src/state/drawings.slice.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 8853cf107..4c2122aab 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -122,10 +122,13 @@ export const drawingsSlice = createSlice({ if (!drawing) { return; } + // Note: the winner is intentionally left untouched here. A chart can + // have both an action (protect/pocket/ban) and a marked winner at the + // same time, and removing the action should not clear the winner. The + // winner is removed via its own control (setWinner with player: null). delete drawing.bans[chartId]; delete drawing.protects[chartId]; delete drawing.pocketPicks[chartId]; - delete drawing.winners[chartId]; }, banProtectReplace( state, From 9c6abc0716df75cd6f4e4016ccc8ccaf665eb333 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 27 Jun 2026 13:57:19 -0700 Subject: [PATCH 157/168] enable yarn global cache --- .yarnrc.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index d91cf2aa7..ba22fa169 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,7 +1,5 @@ compressionLevel: mixed -enableGlobalCache: false - nodeLinker: node-modules yarnPath: .yarn/releases/yarn-4.12.0.cjs From f346cbe2d5b6ee92aed61eba2eff039e3c9d0606 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 04:09:48 +0000 Subject: [PATCH 158/168] Notify users of partykit websocket health and block dispatch while down When the websocket to the party server dies, a persistent danger toast tells users the connection was lost and that changes are disabled. A new middleware drops locally-dispatched redux actions while the connection is down (server-originated messages still pass), surfacing a warning toast if the user attempts a change. On reconnect the toast is replaced with a success notice and dispatch is re-enabled; the server re-sends room state to resync the client. Toasts are suppressed inside OBS browser sources. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AdFh3nxa4T3SdnYgGkDizY --- .claude/skills/verify/SKILL.md | 42 ++++++++++++++++ src/assets/i18n.json | 10 ++++ src/party/client.tsx | 79 +++++++++++++++++++++++++++++- src/party/connection-status.ts | 25 ++++++++++ src/state/party-gate-middleware.ts | 25 ++++++++++ src/state/store.ts | 4 +- 6 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/verify/SKILL.md create mode 100644 src/party/connection-status.ts create mode 100644 src/state/party-gate-middleware.ts diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 000000000..7011d03b7 --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,42 @@ +--- +name: verify +description: Build, launch, and drive DDRCardDraw locally to verify changes at the browser surface. +--- + +# Verifying DDRCardDraw changes + +## Build & launch + +Two dev servers, both from the repo root (partykit needs `partykit.json` in cwd): + +```bash +yarn install +npx partykit dev --port 1999 # websocket/state backend (event mode) +yarn start:frontend # webpack dev server on :8080 +``` + +Wait until `curl http://localhost:1999/parties/main/anyroom` returns 200 (partykit) +and `curl http://localhost:8080/` returns 200 (webpack; first build takes ~1-2 min). + +## Drive + +Playwright (chromium at `/opt/pw-browsers/chromium-*/chrome-linux/chrome` in remote +envs) against: + +- `http://localhost:8080/classic` — classic mode, no backend needed. +- `http://localhost:8080/e/` — event/tournament mode, backed by the + partykit socket. Rooms are created on first visit. + +Useful flows in event mode: + +- Expand the cab panel: click the lone caret button in the 40px-wide left rail. +- Add a cab: fill `input[placeholder="Cab name"]`, press Enter — dispatches a + redux action that round-trips through the partykit socket. +- Kill/restart `partykit dev` to exercise disconnect/reconnect handling + (toasts appear via the app-root OverlayToaster). + +## Gotchas + +- `pkill -f 'partykit dev'` matches its own wrapping shell — use `pkill -f '[p]artykit dev'`. +- Dev `partykit dev` persists room state to disk, so room contents survive a backend restart. +- In dev, the frontend targets `localhost:1999` automatically (`src/party/host.ts`). diff --git a/src/assets/i18n.json b/src/assets/i18n.json index 6deebe913..8a859a8b9 100644 --- a/src/assets/i18n.json +++ b/src/assets/i18n.json @@ -107,6 +107,11 @@ "updateLoading": "Installing an update...", "updateReady": "Update ready, refresh for the freshest code around!", "applyUpdate": "Apply", + "party": { + "disconnected": "Lost connection to the event server. Changes are disabled until it reconnects.", + "reconnected": "Reconnected to the event server!", + "actionBlocked": "Still reconnecting to the event server. Your change was not applied." + }, "error": { "title": "Error Caught!", "description": "A deeply embarassing error just happened somewhere near here. If you're lucky, a reset will fix things up.", @@ -216,6 +221,11 @@ "updateLoading": "更新中...", "updateReady": "更新の準備ができました、このページをリロードしてください!", "applyUpdate": "更新を適用", + "party": { + "disconnected": "イベントサーバーとの接続が切断されました。再接続するまで変更できません。", + "reconnected": "イベントサーバーに再接続しました!", + "actionBlocked": "イベントサーバーに再接続中のため、変更は適用されませんでした。" + }, "error": { "title": "エラーが発生しました!", "description": "恥ずかしながら、どこかでエラーが発生したようです。リセットすることで解決するかもしれません。", diff --git a/src/party/client.tsx b/src/party/client.tsx index 35f48d32a..b27184d3e 100644 --- a/src/party/client.tsx +++ b/src/party/client.tsx @@ -3,19 +3,35 @@ import type { Broadcast, ReduxAction } from "./types"; import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; -import React, { useEffect, useState } from "react"; -import { Card, NonIdealState, Spinner } from "@blueprintjs/core"; +import React, { useEffect, useRef, useState } from "react"; +import { Card, Intent, NonIdealState, Spinner } from "@blueprintjs/core"; +import { Offline } from "@blueprintjs/icons"; import { DelayRender } from "../utils/delay-render"; import { applyMigrations } from "../state/migrations"; import { PARTYKIT_HOST } from "./host"; +import { toaster } from "../toaster"; +import { useIntl } from "../hooks/useIntl"; +import { useInObs } from "../theme-toggle"; +import { + setBlockedActionHandler, + setPartyConnectionHealthy, +} from "./connection-status"; + +const HEALTH_TOAST_KEY = "party-connection-health"; +const BLOCKED_TOAST_KEY = "party-action-blocked"; export function PartySocketManager(props: { roomName?: string; children: React.ReactNode; }) { const dispatch = useAppDispatch(); + const { t } = useIntl(); + const inObs = useInObs(); // TODO move this state to redux??? const [ready, setReady] = useState(false); + // tracks if the user has been notified of a dead connection, + // so we only announce a reconnect after announcing a disconnect + const disconnectedRef = useRef(false); const socket = usePartySocket({ room: props.roomName, host: PARTYKIT_HOST, @@ -40,8 +56,67 @@ export function PartySocketManager(props: { console.warn("failed to handle party socket message", e); } }, + onClose() { + setPartyConnectionHealthy(false); + // before first sync the full-page "Connecting..." state covers this + if (!ready || disconnectedRef.current) { + return; + } + disconnectedRef.current = true; + if (inObs) return; + toaster.show( + { + message: t("party.disconnected"), + icon: , + intent: Intent.DANGER, + timeout: 0, + }, + HEALTH_TOAST_KEY, + ); + }, + onOpen() { + setPartyConnectionHealthy(true); + if (!disconnectedRef.current) { + return; + } + disconnectedRef.current = false; + if (inObs) return; + toaster.dismiss(BLOCKED_TOAST_KEY); + toaster.show( + { + message: t("party.reconnected"), + intent: Intent.SUCCESS, + }, + HEALTH_TOAST_KEY, + ); + }, }); + useEffect(() => { + setBlockedActionHandler(() => { + if (inObs) return; + toaster.show( + { + message: t("party.actionBlocked"), + intent: Intent.WARNING, + }, + BLOCKED_TOAST_KEY, + ); + }); + return () => { + setBlockedActionHandler(undefined); + }; + }, [t, inObs]); + + useEffect(() => { + // when leaving a party session, unblock dispatch for other app modes + return () => { + setPartyConnectionHealthy(true); + toaster.dismiss(HEALTH_TOAST_KEY); + toaster.dismiss(BLOCKED_TOAST_KEY); + }; + }, []); + useEffect(() => { return startAppListening({ predicate(action) { diff --git a/src/party/connection-status.ts b/src/party/connection-status.ts new file mode 100644 index 000000000..55f8d8b03 --- /dev/null +++ b/src/party/connection-status.ts @@ -0,0 +1,25 @@ +/** + * Tracks the health of the active partykit websocket outside of react and + * redux, so store middleware can consult it synchronously. Lives apart from + * app state to avoid being synced to the party server or other clients. + */ + +let healthy = true; +let blockedActionHandler: (() => void) | undefined; + +export function setPartyConnectionHealthy(value: boolean) { + healthy = value; +} + +export function isPartyConnectionHealthy() { + return healthy; +} + +/** registered by the socket manager to give user feedback when an action is dropped */ +export function setBlockedActionHandler(handler: (() => void) | undefined) { + blockedActionHandler = handler; +} + +export function reportBlockedAction() { + blockedActionHandler?.(); +} diff --git a/src/state/party-gate-middleware.ts b/src/state/party-gate-middleware.ts new file mode 100644 index 000000000..c247fa45c --- /dev/null +++ b/src/state/party-gate-middleware.ts @@ -0,0 +1,25 @@ +import type { Middleware } from "@reduxjs/toolkit"; +import { + isPartyConnectionHealthy, + reportBlockedAction, +} from "../party/connection-status"; +import { receivePartyState } from "./central"; + +/** + * Drops locally-dispatched actions while the partykit websocket is down, + * so local state can't silently drift from the shared room state. + */ +export const partyGateMiddleware: Middleware = () => (next) => (action) => { + if (isPartyConnectionHealthy()) { + return next(action); + } + // always let server-originated messages through, in case any arrive + // before the reconnect handler flips the flag back + if ( + receivePartyState.match(action) || + (action as { meta?: { source?: string } }).meta?.source === "partykit" + ) { + return next(action); + } + reportBlockedAction(); +}; diff --git a/src/state/store.ts b/src/state/store.ts index dc14d8185..c9998d688 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -7,11 +7,13 @@ import { import { useDispatch, useSelector, useStore } from "react-redux"; import { reducer, type AppState } from "./root-reducer"; import { middleware as listener } from "./listener-middleware"; +import { partyGateMiddleware } from "./party-gate-middleware"; export function createClientStore(preloadedState?: AppState) { return configureStore({ reducer, - middleware: (getDefaults) => getDefaults().concat(listener), + middleware: (getDefaults) => + getDefaults().concat(partyGateMiddleware, listener), preloadedState, }); } From 9f415c30e0bc26ec83fa8a85dc4bc13dac1ef57c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 04:27:54 +0000 Subject: [PATCH 159/168] Add receipt confirmation and re-send to the partykit action protocol Clients now tag each outgoing action with a unique message id, and the server confirms receipt with an ack after applying and persisting it. A client-side tracker re-sends any action that goes unconfirmed for 5 seconds (up to 4 transmissions on a live connection, then a danger toast announces the change was abandoned). The server remembers recently applied action ids so re-sends are acked again but never applied twice. Actions still unconfirmed across a reconnect are replayed after the fresh roomstate is applied: the server now includes its recently applied ids in the roomstate message so replays that actually landed before the disconnect are dropped instead of duplicated. Dispatch now stays gated from disconnect until this resync completes (previously it reopened on socket-open, leaving a window where a local action could be silently erased by the arriving roomstate). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AdFh3nxa4T3SdnYgGkDizY --- .claude/skills/verify/SKILL.md | 5 ++ src/assets/i18n.json | 6 +- src/party/client.tsx | 83 +++++++++++++++++------- src/party/pending-actions.ts | 115 +++++++++++++++++++++++++++++++++ src/party/server.ts | 38 ++++++++++- src/party/types.ts | 15 ++++- 6 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 src/party/pending-actions.ts diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 7011d03b7..4de20922d 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -38,5 +38,10 @@ Useful flows in event mode: ## Gotchas - `pkill -f 'partykit dev'` matches its own wrapping shell — use `pkill -f '[p]artykit dev'`. +- `partykit dev` spawns a `workerd` child that actually owns port 1999 and the + websockets. To simulate a stalled/dead server, signal `'[w]orkerd serve'` + (SIGSTOP freezes it without closing sockets — great for ack-timeout tests); + killing only the node wrapper leaves the server running. Playwright's + `page.on("websocket")` frame events give protocol-level evidence. - Dev `partykit dev` persists room state to disk, so room contents survive a backend restart. - In dev, the frontend targets `localhost:1999` automatically (`src/party/host.ts`). diff --git a/src/assets/i18n.json b/src/assets/i18n.json index 8a859a8b9..8e43247f3 100644 --- a/src/assets/i18n.json +++ b/src/assets/i18n.json @@ -110,7 +110,8 @@ "party": { "disconnected": "Lost connection to the event server. Changes are disabled until it reconnects.", "reconnected": "Reconnected to the event server!", - "actionBlocked": "Still reconnecting to the event server. Your change was not applied." + "actionBlocked": "Still reconnecting to the event server. Your change was not applied.", + "sendFailed": "A change could not be delivered to the event server after several attempts." }, "error": { "title": "Error Caught!", @@ -224,7 +225,8 @@ "party": { "disconnected": "イベントサーバーとの接続が切断されました。再接続するまで変更できません。", "reconnected": "イベントサーバーに再接続しました!", - "actionBlocked": "イベントサーバーに再接続中のため、変更は適用されませんでした。" + "actionBlocked": "イベントサーバーに再接続中のため、変更は適用されませんでした。", + "sendFailed": "複数回試みましたが、変更をイベントサーバーへ送信できませんでした。" }, "error": { "title": "エラーが発生しました!", diff --git a/src/party/client.tsx b/src/party/client.tsx index b27184d3e..235cc5c32 100644 --- a/src/party/client.tsx +++ b/src/party/client.tsx @@ -1,5 +1,5 @@ import usePartySocket from "partysocket/react"; -import type { Broadcast, ReduxAction } from "./types"; +import type { Broadcast } from "./types"; import { useAppDispatch } from "../state/store"; import { receivePartyState } from "../state/central"; import { startAppListening } from "../state/listener-middleware"; @@ -16,9 +16,11 @@ import { setBlockedActionHandler, setPartyConnectionHealthy, } from "./connection-status"; +import { PendingActionTracker } from "./pending-actions"; const HEALTH_TOAST_KEY = "party-connection-health"; const BLOCKED_TOAST_KEY = "party-action-blocked"; +const SEND_FAILED_TOAST_KEY = "party-action-send-failed"; export function PartySocketManager(props: { roomName?: string; @@ -32,6 +34,11 @@ export function PartySocketManager(props: { // tracks if the user has been notified of a dead connection, // so we only announce a reconnect after announcing a disconnect const disconnectedRef = useRef(false); + const trackerRef = useRef(null); + // keeps the tracker's give-up toast bound to the current locale without + // recreating the tracker (which would drop pending actions) + const sendFailedToast = useRef(() => {}); + const socket = usePartySocket({ room: props.roomName, host: PARTYKIT_HOST, @@ -42,6 +49,24 @@ export function PartySocketManager(props: { case "roomstate": applyMigrations(data.state); dispatch(receivePartyState(data.state)); + // replay anything that went unconfirmed before this (re)connect, + // now that the fresh server state has been applied locally + trackerRef.current?.handleRoomstate(data.recentActionIds ?? []); + // dispatch stays blocked until the resync above is complete + setPartyConnectionHealthy(true); + if (disconnectedRef.current) { + disconnectedRef.current = false; + if (!inObs) { + toaster.dismiss(BLOCKED_TOAST_KEY); + toaster.show( + { + message: t("party.reconnected"), + intent: Intent.SUCCESS, + }, + HEALTH_TOAST_KEY, + ); + } + } setReady(true); break; case "action": @@ -51,6 +76,9 @@ export function PartySocketManager(props: { }; dispatch(foreignAction); break; + case "ack": + trackerRef.current?.handleAck(data.id); + break; } } catch (e) { console.warn("failed to handle party socket message", e); @@ -74,22 +102,6 @@ export function PartySocketManager(props: { HEALTH_TOAST_KEY, ); }, - onOpen() { - setPartyConnectionHealthy(true); - if (!disconnectedRef.current) { - return; - } - disconnectedRef.current = false; - if (inObs) return; - toaster.dismiss(BLOCKED_TOAST_KEY); - toaster.show( - { - message: t("party.reconnected"), - intent: Intent.SUCCESS, - }, - HEALTH_TOAST_KEY, - ); - }, }); useEffect(() => { @@ -103,6 +115,16 @@ export function PartySocketManager(props: { BLOCKED_TOAST_KEY, ); }); + sendFailedToast.current = () => { + if (inObs) return; + toaster.show( + { + message: t("party.sendFailed"), + intent: Intent.DANGER, + }, + SEND_FAILED_TOAST_KEY, + ); + }; return () => { setBlockedActionHandler(undefined); }; @@ -114,11 +136,23 @@ export function PartySocketManager(props: { setPartyConnectionHealthy(true); toaster.dismiss(HEALTH_TOAST_KEY); toaster.dismiss(BLOCKED_TOAST_KEY); + toaster.dismiss(SEND_FAILED_TOAST_KEY); }; }, []); useEffect(() => { - return startAppListening({ + const tracker = new PendingActionTracker(socket, { + redispatch(action) { + // mirror the shape of relayed actions so the listener below + // doesn't send it to the server a second time + dispatch({ ...action, meta: { source: "partykit" } }); + }, + onGiveUp() { + sendFailedToast.current(); + }, + }); + trackerRef.current = tracker; + const stopListening = startAppListening({ predicate(action) { // @ts-expect-error i don't know how to type action meta properties yet if (action.meta?.source === "partykit") { @@ -132,14 +166,15 @@ export function PartySocketManager(props: { return true; }, effect(action) { - const message: ReduxAction = { - type: "action", - action, - }; - socket.send(JSON.stringify(message)); + tracker.send(action); }, }); - }, [socket]); + return () => { + stopListening(); + tracker.dispose(); + trackerRef.current = null; + }; + }, [socket, dispatch]); if (!ready) { return ( diff --git a/src/party/pending-actions.ts b/src/party/pending-actions.ts new file mode 100644 index 000000000..be6962555 --- /dev/null +++ b/src/party/pending-actions.ts @@ -0,0 +1,115 @@ +import { nanoid } from "nanoid"; +import type { Action } from "@reduxjs/toolkit"; +import type { ReduxAction } from "./types"; + +/** how long to wait for the server to confirm receipt before re-sending */ +const ACK_TIMEOUT_MS = 5000; +/** total transmissions per action on a live connection before giving up */ +const MAX_SEND_ATTEMPTS = 4; + +interface SocketLike { + readyState: number; + send(data: string): void; +} + +interface PendingEntry { + message: Required; + attempts: number; + timer?: ReturnType; +} + +/** + * Tracks actions sent to the party server until receipt is confirmed. + * Unconfirmed sends are re-transmitted on a timeout (the server dedupes by + * message id), and anything still pending when a fresh roomstate arrives + * after a reconnect is replayed both locally and over the wire. + */ +export class PendingActionTracker { + private pending = new Map(); + + constructor( + private socket: SocketLike, + private handlers: { + /** re-applies a replayed action to the local store after a roomstate reset */ + redispatch: (action: Action) => void; + /** called when an action is abandoned after repeated unconfirmed sends */ + onGiveUp: (action: Action) => void; + }, + ) {} + + /** send an action to the server, tracking it until receipt is confirmed */ + send(action: Action) { + const entry: PendingEntry = { + message: { type: "action", action, id: nanoid() }, + attempts: 0, + }; + this.pending.set(entry.message.id, entry); + this.transmit(entry); + } + + /** the server confirmed receipt of this action */ + handleAck(id: string) { + const entry = this.pending.get(id); + if (!entry) return; + clearTimeout(entry.timer); + this.pending.delete(id); + } + + /** + * A fresh roomstate arrived, overwriting local optimistic updates. Drop + * pending actions the server reports as already applied (their effects are + * baked into the new state) and replay the rest, both into the local store + * and over the wire. + */ + handleRoomstate(appliedActionIds: string[]) { + const applied = new Set(appliedActionIds); + for (const entry of Array.from(this.pending.values())) { + clearTimeout(entry.timer); + if (applied.has(entry.message.id)) { + this.pending.delete(entry.message.id); + continue; + } + entry.attempts = 0; + this.handlers.redispatch(entry.message.action); + this.transmit(entry); + } + } + + /** count of actions awaiting confirmation */ + get pendingCount() { + return this.pending.size; + } + + dispose() { + for (const entry of this.pending.values()) { + clearTimeout(entry.timer); + } + this.pending.clear(); + } + + private transmit(entry: PendingEntry) { + if (this.socket.readyState === WebSocket.OPEN) { + entry.attempts += 1; + this.socket.send(JSON.stringify(entry.message)); + } + entry.timer = setTimeout( + () => this.handleAckTimeout(entry), + ACK_TIMEOUT_MS, + ); + } + + private handleAckTimeout(entry: PendingEntry) { + if (!this.pending.has(entry.message.id)) return; + if (this.socket.readyState !== WebSocket.OPEN) { + // connection is down: hold the action for replay after the reconnect, + // which arrives via handleRoomstate + return; + } + if (entry.attempts >= MAX_SEND_ATTEMPTS) { + this.pending.delete(entry.message.id); + this.handlers.onGiveUp(entry.message.action); + return; + } + this.transmit(entry); + } +} diff --git a/src/party/server.ts b/src/party/server.ts index 7372559f2..77b48d5ff 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -1,5 +1,5 @@ import type * as Party from "partykit/server"; -import type { ReduxAction, Roomstate } from "./types"; +import type { ActionAck, ReduxAction, Roomstate } from "./types"; import { configureStore } from "@reduxjs/toolkit"; import { reducer } from "../state/root-reducer"; import type { AppState } from "../state/store"; @@ -30,10 +30,16 @@ function isAppState(state: unknown): state is AppState { return false; } +/** upper bound on remembered action ids used to dedupe client re-sends */ +const MAX_REMEMBERED_ACTIONS = 1000; + export default class Server implements Party.Server { // @ts-expect-error I assign this for sure private store: typeof appReduxStore; + /** ids of recently applied actions, oldest first */ + private seenActionIds = new Set(); + constructor(readonly room: Party.Room) { console.log("constructor start"); } @@ -93,11 +99,21 @@ export default class Server implements Party.Server { JSON.stringify({ type: "roomstate", state: this.store.getState(), + recentActionIds: Array.from(this.seenActionIds), }), ); } async onMessage(message: string, sender: Party.Connection) { + const parsed = JSON.parse(message) as ReduxAction; + + if (parsed.id && this.seenActionIds.has(parsed.id)) { + // a re-send of an action already applied: confirm receipt again + // (the original ack may have been lost) but don't apply it twice + this.sendAck(sender, parsed.id); + return; + } + // broadcast it to all the other connections in the room... this.room.broadcast( message, @@ -105,12 +121,16 @@ export default class Server implements Party.Server { [sender.id], ); - const parsed = JSON.parse(message) as ReduxAction; // resolve the new state this.store.dispatch(parsed.action); const nextState = this.store.getState(); // persist to partykit storage void this.room.storage.put("currentState", nextState); + + if (parsed.id) { + this.rememberActionId(parsed.id); + this.sendAck(sender, parsed.id); + } // persist the state to supabase try { if (supabase) { @@ -124,6 +144,20 @@ export default class Server implements Party.Server { console.warn("error with upsert", e); } } + + private sendAck(conn: Party.Connection, id: string) { + conn.send(JSON.stringify({ type: "ack", id })); + } + + private rememberActionId(id: string) { + this.seenActionIds.add(id); + if (this.seenActionIds.size > MAX_REMEMBERED_ACTIONS) { + for (const oldest of this.seenActionIds) { + this.seenActionIds.delete(oldest); + break; + } + } + } } Server satisfies Party.Worker; diff --git a/src/party/types.ts b/src/party/types.ts index 7aec1910e..9bd43424a 100644 --- a/src/party/types.ts +++ b/src/party/types.ts @@ -4,12 +4,25 @@ import { AppState } from "../state/store"; export interface Roomstate { type: "roomstate"; state: AppState; + /** + * ids of recently applied actions, so reconnecting clients can drop + * pending re-sends whose effects are already baked into `state` + */ + recentActionIds?: string[]; } export interface ReduxAction { type: "action"; action: Action; + /** unique message id; when present the server confirms receipt with an ack */ + id?: string; +} + +/** the server confirms it received and applied the action with this id */ +export interface ActionAck { + type: "ack"; + id: string; } /** All messages possibly sent by the server to clients */ -export type Broadcast = Roomstate | ReduxAction; +export type Broadcast = Roomstate | ReduxAction | ActionAck; From 775612c6e2bd25599884e9cf471fee1f85baa181 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 05:14:55 +0000 Subject: [PATCH 160/168] Sequence all actions through the server for convergent replication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements step 1 of docs/partykit-sync-roadmap.md: the party server stamps every accepted action with a monotonic seq and broadcasts it to all clients including the sender — the echo replaces the separate ack as receipt confirmation. Clients (SyncManager, formerly PendingActionTracker) now maintain a confirmed state built only from server-ordered actions, with unconfirmed local actions rebased on top, so concurrent edits from multiple clients converge to identical state in canonical order instead of applying in whatever order each replica saw. A seq gap (missed broadcast) triggers a reconnect for a fresh roomstate, and giving up on an undeliverable action now also rolls it back out of the local display state. Also fixes event/addCab minting its cab id inside the reducer: replayed actions must be deterministic, so the id now rides in the payload via a prepare callback. Previously the sender's local cab id never matched the id the server and other clients generated for the same action. Legacy interop: id-less actions from pre-protocol clients are relayed unstamped excluding the sender (no echo they can't recognize), and a seq-less server downgrades new clients to plain optimistic apply. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AdFh3nxa4T3SdnYgGkDizY --- docs/partykit-sync-roadmap.md | 150 ++++++++++++++++++++++++ src/party/client.tsx | 48 ++++---- src/party/pending-actions.ts | 115 ------------------- src/party/server.ts | 33 ++++-- src/party/sync-manager.ts | 208 ++++++++++++++++++++++++++++++++++ src/party/types.ts | 8 ++ src/state/event.slice.ts | 21 ++-- 7 files changed, 427 insertions(+), 156 deletions(-) create mode 100644 docs/partykit-sync-roadmap.md delete mode 100644 src/party/pending-actions.ts create mode 100644 src/party/sync-manager.ts diff --git a/docs/partykit-sync-roadmap.md b/docs/partykit-sync-roadmap.md new file mode 100644 index 000000000..d0c4779b1 --- /dev/null +++ b/docs/partykit-sync-roadmap.md @@ -0,0 +1,150 @@ +# Event-mode sync: architecture roadmap + +This documents the design plan for evolving the partykit-based sync behind +event mode (`/e/:roomName`) into something robust enough for flaky venue +wifi and concurrent editing, **without abandoning redux**. It was written +alongside PR #604 so the work can be picked up later from a fresh context. + +## Where this is going, in one paragraph + +Redux is a state machine, and PartyKit gives every room a single always-online +actor. That combination is the textbook setup for a **replicated state machine +with a central sequencer**: clients send actions as _proposals_, the server +orders them (assigning a monotonic `seq`), and every replica — including the +proposer — builds its confirmed state exclusively from server-ordered actions. +Local responsiveness comes from an optimistic "pending" layer rebased on top. +This gets convergence, offline detection, and incremental recovery while +keeping plain-JSON state, ordinary reducers, and centrally-enforced +invariants. CRDTs are deliberately **not** on this path (see last section). + +## Current architecture (as of PR #604) + +- Client and server run the same reducer bundle (`src/state/root-reducer.ts`). + The server's store is authoritative; full snapshots persist to room storage + and Supabase. +- Wire protocol (`src/party/types.ts`): + - client → server: `{type: "action", action, id}` — `id` is a unique + message id (nanoid). + - server → everyone (including sender): the same action stamped with a + monotonic `seq`. The echo doubles as the receipt confirmation (ack). + - server → sender only: `{type: "ack", id, ...}` when a duplicate re-send + arrives for an already-applied id. + - server → client on connect: `{type: "roomstate", state, seq, +recentActionIds}`. +- Client-side `SyncManager` (`src/party/sync-manager.ts`) maintains the core + invariant: **display state == confirmed state + pending actions replayed in + order**. + - _confirmed_: built only from seq-stamped actions, applied in seq order. + - _pending_: locally-dispatched actions not yet confirmed; re-sent every 5s + (server dedupes by id), abandoned after 4 attempts with a toast + local + rollback (rebase without the abandoned action). + - A foreign action arriving while pending actions exist triggers a rebase: + recompute display = confirmed + pending, delivered via + `receivePartyState` (wholesale state replacement in the root reducer). + - A `seq` gap means a missed broadcast; the only repair today is + `socket.reconnect()` → fresh roomstate (see step 2 for the cheaper fix). + - On roomstate: pending ids listed in `recentActionIds` are dropped (their + effects are baked into the snapshot); the rest are rebased and re-sent. +- Connection health: dispatch is gated off (`partyGateMiddleware`) from + socket-close until the post-reconnect roomstate is fully applied; users see + disconnect / blocked / reconnected toasts (suppressed in OBS sources). + +### Hard requirement: deterministic reducers + +Replication by action replay only converges if the same action produces the +same state everywhere. **No `nanoid()`, `Math.random()`, or `Date.now()` +inside reducers** — generate ids/timestamps in `prepare` callbacks or thunks +so they ride in the action payload. `event/addCab` violated this (id minted +in the reducer, so the sender's cab id never matched the server's) and was +fixed in PR #604. Audit any new reducer for this. + +### Version compatibility rules + +- Old (pre-#604) clients send actions without `id`. The server relays those + the old way (broadcast excluding sender, no echo, no stamp) so they don't + double-apply their own actions. New clients apply un-stamped foreign + actions to both confirmed and display (server broadcast order is still + canonical), skipping seq checks. +- A new client on an old server (no `seq` in roomstate) degrades to the + pre-#604 behavior: optimistic apply + ack-based pending tracking, no + rebasing, no give-up rollback (`lastSeq == null` guards these). +- **Deploy the partykit server before the web app** whenever the protocol + grows. + +## Roadmap + +### Step 1 — server sequencing + confirmed/pending split ✅ (PR #604) + +Described above. Kills the divergence class caused by clients applying +concurrent actions in different orders. + +### Step 2 — incremental catch-up + heartbeat (next) + +- Server keeps a tail of recent stamped actions (say 500) in room storage. + Client sends `{type: "catchup", since: seq}` on a detected gap; server + replies with the missing stamped actions (or a full roomstate if the tail + doesn't reach back far enough). Replaces reconnect-as-only-repair; makes + brief drops nearly free. +- Application-level heartbeat: client pings every ~10s, treats N missed pongs + as a dead connection and forces the reconnect flow. Today a + stalled-but-open socket (server frozen, half-open TCP) isn't noticed until + an ack timeout — verified with SIGSTOP on workerd, see + `.claude/skills/verify/SKILL.md`. +- Move `seq` and the dedupe id set into room storage so PartyKit hibernation + or a server restart can't reset them (today: in-memory; safe only because a + restart drops all sockets, forcing full roomstate resyncs; the dedupe + window across hibernation is a known small hole). + +### Step 3 — event-sourcing lite + +- Persist the stamped action log + periodic snapshots as the durable format. + Supabase gets snapshot + tail instead of a full-state upsert on every + action (current write amplification is significant). +- Tag log entries with app version; snapshot on version bump so old actions + never replay through new reducers (`applyMigrations` stays snapshot-only). +- Unlocks: undo, audit ("who deleted that drawing"), time-travel debugging. +- Add `{type: "hello", protocolVersion}` handshake; server can tell outdated + clients to refresh via the existing update-manager flow. + +### Step 4 — trust boundary + +- Server currently dispatches whatever clients send. Notably a forged + `party/supplyState` action would overwrite the whole room via the root + reducer's state-replacement branch. Whitelist allowed action types + server-side. +- Room secret / role tokens: organizer (write) vs viewer + OBS sources + (read-only). + +### Step 5 (only if requirements change) — scoped CRDTs + +Full-CRDT (automerge/yjs for the whole state) is deliberately rejected: + +- PartyKit already provides the central authority CRDTs exist to avoid + needing; the sequencer gets convergence with none of the CRDT costs. +- Costs avoided: rewriting redux slices as CRDT docs, tombstone growth, + losing plain-JSON state, and — decisive — invariants like "one active + match per cab" being unenforceable under merge semantics. The reducer + stays the single place business rules live. +- MQTT for comparison: its useful ideas are already absorbed — the ack/dedupe + work is QoS-1 + idempotency (effectively exactly-once application), + roomstate-on-connect is a retained message, step 2's catch-up is session + resumption. An external broker would add ops burden without solving + ordering or merge. + +If true offline editing ever becomes a requirement, scope a CRDT to the one +subtree that wants it (e.g. collaborative text editing of `obsCss` via yjs +running through its own channel), not the whole state. Queued-intent replay +(the pending layer) already covers short offline windows. + +## File map + +| Concern | File | +| ----------------------------------- | ------------------------------------------------------------ | +| Wire protocol types | `src/party/types.ts` | +| Server (room actor) | `src/party/server.ts` | +| Client socket manager (react) | `src/party/client.tsx` | +| Confirmed/pending sync manager | `src/party/sync-manager.ts` | +| Dispatch gate while disconnected | `src/state/party-gate-middleware.ts` | +| Connection health flag (non-synced) | `src/party/connection-status.ts` | +| Full-state replacement action | `receivePartyState` in `src/state/central.ts` + root reducer | +| Runtime verification recipe | `.claude/skills/verify/SKILL.md` | diff --git a/src/party/client.tsx b/src/party/client.tsx index 235cc5c32..94c3d3201 100644 --- a/src/party/client.tsx +++ b/src/party/client.tsx @@ -16,7 +16,7 @@ import { setBlockedActionHandler, setPartyConnectionHealthy, } from "./connection-status"; -import { PendingActionTracker } from "./pending-actions"; +import { SyncManager } from "./sync-manager"; const HEALTH_TOAST_KEY = "party-connection-health"; const BLOCKED_TOAST_KEY = "party-action-blocked"; @@ -34,9 +34,9 @@ export function PartySocketManager(props: { // tracks if the user has been notified of a dead connection, // so we only announce a reconnect after announcing a disconnect const disconnectedRef = useRef(false); - const trackerRef = useRef(null); - // keeps the tracker's give-up toast bound to the current locale without - // recreating the tracker (which would drop pending actions) + const syncRef = useRef(null); + // keeps the sync manager's give-up toast bound to the current locale + // without recreating it (which would drop pending actions) const sendFailedToast = useRef(() => {}); const socket = usePartySocket({ @@ -48,10 +48,13 @@ export function PartySocketManager(props: { switch (data.type) { case "roomstate": applyMigrations(data.state); - dispatch(receivePartyState(data.state)); - // replay anything that went unconfirmed before this (re)connect, - // now that the fresh server state has been applied locally - trackerRef.current?.handleRoomstate(data.recentActionIds ?? []); + // adopt the server state as confirmed, rebasing anything that + // went unconfirmed before this (re)connect on top of it + dispatch( + receivePartyState( + syncRef.current?.handleRoomstate(data) ?? data.state, + ), + ); // dispatch stays blocked until the resync above is complete setPartyConnectionHealthy(true); if (disconnectedRef.current) { @@ -70,14 +73,10 @@ export function PartySocketManager(props: { setReady(true); break; case "action": - const foreignAction = { - ...data.action, - meta: { source: "partykit" }, - }; - dispatch(foreignAction); + syncRef.current?.handleRemoteAction(data); break; case "ack": - trackerRef.current?.handleAck(data.id); + syncRef.current?.handleAck(data.id); break; } } catch (e) { @@ -141,17 +140,22 @@ export function PartySocketManager(props: { }, []); useEffect(() => { - const tracker = new PendingActionTracker(socket, { - redispatch(action) { - // mirror the shape of relayed actions so the listener below - // doesn't send it to the server a second time + const sync = new SyncManager(socket, { + dispatchForeign(action) { + // mark the source so the listener below doesn't send it back out dispatch({ ...action, meta: { source: "partykit" } }); }, + applyState(state) { + dispatch(receivePartyState(state)); + }, + resync() { + socket.reconnect(); + }, onGiveUp() { sendFailedToast.current(); }, }); - trackerRef.current = tracker; + syncRef.current = sync; const stopListening = startAppListening({ predicate(action) { // @ts-expect-error i don't know how to type action meta properties yet @@ -166,13 +170,13 @@ export function PartySocketManager(props: { return true; }, effect(action) { - tracker.send(action); + sync.send(action); }, }); return () => { stopListening(); - tracker.dispose(); - trackerRef.current = null; + sync.dispose(); + syncRef.current = null; }; }, [socket, dispatch]); diff --git a/src/party/pending-actions.ts b/src/party/pending-actions.ts deleted file mode 100644 index be6962555..000000000 --- a/src/party/pending-actions.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { nanoid } from "nanoid"; -import type { Action } from "@reduxjs/toolkit"; -import type { ReduxAction } from "./types"; - -/** how long to wait for the server to confirm receipt before re-sending */ -const ACK_TIMEOUT_MS = 5000; -/** total transmissions per action on a live connection before giving up */ -const MAX_SEND_ATTEMPTS = 4; - -interface SocketLike { - readyState: number; - send(data: string): void; -} - -interface PendingEntry { - message: Required; - attempts: number; - timer?: ReturnType; -} - -/** - * Tracks actions sent to the party server until receipt is confirmed. - * Unconfirmed sends are re-transmitted on a timeout (the server dedupes by - * message id), and anything still pending when a fresh roomstate arrives - * after a reconnect is replayed both locally and over the wire. - */ -export class PendingActionTracker { - private pending = new Map(); - - constructor( - private socket: SocketLike, - private handlers: { - /** re-applies a replayed action to the local store after a roomstate reset */ - redispatch: (action: Action) => void; - /** called when an action is abandoned after repeated unconfirmed sends */ - onGiveUp: (action: Action) => void; - }, - ) {} - - /** send an action to the server, tracking it until receipt is confirmed */ - send(action: Action) { - const entry: PendingEntry = { - message: { type: "action", action, id: nanoid() }, - attempts: 0, - }; - this.pending.set(entry.message.id, entry); - this.transmit(entry); - } - - /** the server confirmed receipt of this action */ - handleAck(id: string) { - const entry = this.pending.get(id); - if (!entry) return; - clearTimeout(entry.timer); - this.pending.delete(id); - } - - /** - * A fresh roomstate arrived, overwriting local optimistic updates. Drop - * pending actions the server reports as already applied (their effects are - * baked into the new state) and replay the rest, both into the local store - * and over the wire. - */ - handleRoomstate(appliedActionIds: string[]) { - const applied = new Set(appliedActionIds); - for (const entry of Array.from(this.pending.values())) { - clearTimeout(entry.timer); - if (applied.has(entry.message.id)) { - this.pending.delete(entry.message.id); - continue; - } - entry.attempts = 0; - this.handlers.redispatch(entry.message.action); - this.transmit(entry); - } - } - - /** count of actions awaiting confirmation */ - get pendingCount() { - return this.pending.size; - } - - dispose() { - for (const entry of this.pending.values()) { - clearTimeout(entry.timer); - } - this.pending.clear(); - } - - private transmit(entry: PendingEntry) { - if (this.socket.readyState === WebSocket.OPEN) { - entry.attempts += 1; - this.socket.send(JSON.stringify(entry.message)); - } - entry.timer = setTimeout( - () => this.handleAckTimeout(entry), - ACK_TIMEOUT_MS, - ); - } - - private handleAckTimeout(entry: PendingEntry) { - if (!this.pending.has(entry.message.id)) return; - if (this.socket.readyState !== WebSocket.OPEN) { - // connection is down: hold the action for replay after the reconnect, - // which arrives via handleRoomstate - return; - } - if (entry.attempts >= MAX_SEND_ATTEMPTS) { - this.pending.delete(entry.message.id); - this.handlers.onGiveUp(entry.message.action); - return; - } - this.transmit(entry); - } -} diff --git a/src/party/server.ts b/src/party/server.ts index 77b48d5ff..aca5fbb7d 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -37,8 +37,11 @@ export default class Server implements Party.Server { // @ts-expect-error I assign this for sure private store: typeof appReduxStore; - /** ids of recently applied actions, oldest first */ - private seenActionIds = new Set(); + /** monotonic counter assigning the canonical order of applied actions */ + private seq = 0; + + /** recently applied action ids mapped to their seq, oldest first */ + private seenActionIds = new Map(); constructor(readonly room: Party.Room) { console.log("constructor start"); @@ -99,7 +102,8 @@ export default class Server implements Party.Server { JSON.stringify({ type: "roomstate", state: this.store.getState(), - recentActionIds: Array.from(this.seenActionIds), + recentActionIds: Array.from(this.seenActionIds.keys()), + seq: this.seq, }), ); } @@ -114,12 +118,18 @@ export default class Server implements Party.Server { return; } - // broadcast it to all the other connections in the room... - this.room.broadcast( - message, - // ...except for the connection it came from - [sender.id], - ); + if (parsed.id) { + // stamp the action with its canonical position and broadcast to + // everyone *including* the sender: the echo doubles as the receipt + // confirmation, and all replicas apply actions in seq order + this.seq += 1; + const stamped: ReduxAction = { ...parsed, seq: this.seq }; + this.room.broadcast(JSON.stringify(stamped)); + } else { + // legacy client that can't recognize its own echo: relay the + // unstamped action to everyone else only + this.room.broadcast(message, [sender.id]); + } // resolve the new state this.store.dispatch(parsed.action); @@ -129,7 +139,6 @@ export default class Server implements Party.Server { if (parsed.id) { this.rememberActionId(parsed.id); - this.sendAck(sender, parsed.id); } // persist the state to supabase try { @@ -150,9 +159,9 @@ export default class Server implements Party.Server { } private rememberActionId(id: string) { - this.seenActionIds.add(id); + this.seenActionIds.set(id, this.seq); if (this.seenActionIds.size > MAX_REMEMBERED_ACTIONS) { - for (const oldest of this.seenActionIds) { + for (const oldest of this.seenActionIds.keys()) { this.seenActionIds.delete(oldest); break; } diff --git a/src/party/sync-manager.ts b/src/party/sync-manager.ts new file mode 100644 index 000000000..dde33512e --- /dev/null +++ b/src/party/sync-manager.ts @@ -0,0 +1,208 @@ +import { nanoid } from "nanoid"; +import type { Action } from "@reduxjs/toolkit"; +import { reducer } from "../state/root-reducer"; +import type { AppState } from "../state/store"; +import type { ReduxAction, Roomstate } from "./types"; + +/** how long to wait for the server to confirm receipt before re-sending */ +const ACK_TIMEOUT_MS = 5000; +/** total transmissions per action on a live connection before giving up */ +const MAX_SEND_ATTEMPTS = 4; + +interface SocketLike { + readyState: number; + send(data: string): void; +} + +interface PendingEntry { + message: ReduxAction & { id: string }; + attempts: number; + timer?: ReturnType; +} + +/** + * Client half of the replicated state machine. + * + * Maintains the invariant: display state == confirmed state + pending + * actions replayed in order. + * + * - `confirmed` is built exclusively from server-ordered (seq-stamped) + * actions, so every replica converges on the same state. + * - `pending` holds locally-dispatched actions awaiting the server's echo. + * They're already applied to the display store optimistically; unconfirmed + * sends re-transmit on a timeout (the server dedupes by id) and anything + * still pending across a reconnect is rebased onto the fresh roomstate. + * + * Against a server that predates seq stamping (`lastSeq == null`), this + * degrades to plain optimistic apply with ack-based pending tracking: + * no rebasing, no give-up rollback. + */ +export class SyncManager { + private pending = new Map(); + private confirmed: AppState | null = null; + private lastSeq: number | null = null; + /** true from detecting a missed broadcast until the repairing roomstate */ + private resyncing = false; + + constructor( + private socket: SocketLike, + private handlers: { + /** apply a foreign action directly to the display store */ + dispatchForeign: (action: Action) => void; + /** replace the display store state wholesale after a rebase */ + applyState: (state: AppState) => void; + /** a broadcast was missed; only a fresh roomstate can repair us */ + resync: () => void; + /** an action was abandoned after repeated unconfirmed sends */ + onGiveUp: (action: Action) => void; + }, + ) {} + + /** send a locally-dispatched action (already applied to the display store) */ + send(action: Action) { + const entry: PendingEntry = { + message: { type: "action", action, id: nanoid() }, + attempts: 0, + }; + this.pending.set(entry.message.id, entry); + this.transmit(entry); + } + + /** + * A fresh roomstate arrived (initial connect, reconnect, or repair). + * Adopts it as the confirmed state, drops pending actions the server + * already applied, and re-sends the rest. Returns the state the display + * store should show: confirmed with remaining pending rebased on top. + */ + handleRoomstate(roomstate: Roomstate): AppState { + this.resyncing = false; + this.confirmed = roomstate.state; + this.lastSeq = roomstate.seq ?? null; + const applied = new Set(roomstate.recentActionIds ?? []); + for (const entry of Array.from(this.pending.values())) { + clearTimeout(entry.timer); + if (applied.has(entry.message.id)) { + // its effect is already baked into the snapshot + this.pending.delete(entry.message.id); + continue; + } + entry.attempts = 0; + this.transmit(entry); + } + return this.rebase(); + } + + /** + * An action broadcast arrived — our own echoed back, or another client's. + * Advances the confirmed state and keeps the display state consistent. + */ + handleRemoteAction(message: ReduxAction) { + if (this.resyncing) return; + if (!this.confirmed) { + // roomstate always precedes broadcasts on a connection, so this is + // unreachable; degrade gracefully if it somehow isn't + if (!(message.id && this.pending.has(message.id))) { + this.handlers.dispatchForeign(message.action); + } + return; + } + if (message.seq != null && this.lastSeq != null) { + if (message.seq <= this.lastSeq) { + // stale re-delivery of something already reflected in confirmed + if (message.id) this.settle(message.id); + return; + } + if (message.seq > this.lastSeq + 1) { + this.resyncing = true; + this.handlers.resync(); + return; + } + } + if (message.seq != null) { + this.lastSeq = message.seq; + } + this.confirmed = reducer(this.confirmed, message.action); + + const ownEntry = message.id ? this.pending.get(message.id) : undefined; + if (ownEntry) { + const wasFirst = this.pending.keys().next().value === message.id; + this.settle(message.id!); + // the display store already reflects our own action optimistically; + // unless the stream confirmed it out of order, no dispatch is needed + if (!wasFirst) { + this.handlers.applyState(this.rebase()); + } + return; + } + if (this.pending.size === 0) { + // no local speculation: the foreign action applies directly + this.handlers.dispatchForeign(message.action); + } else { + // a foreign action was ordered underneath our unconfirmed ones + this.handlers.applyState(this.rebase()); + } + } + + /** the server says this id was applied earlier (duplicate re-send) */ + handleAck(id: string) { + this.settle(id); + } + + /** count of actions awaiting confirmation */ + get pendingCount() { + return this.pending.size; + } + + dispose() { + for (const entry of this.pending.values()) { + clearTimeout(entry.timer); + } + this.pending.clear(); + } + + /** replay pending actions over the confirmed state */ + private rebase(): AppState { + let state = this.confirmed!; + for (const entry of this.pending.values()) { + state = reducer(state, entry.message.action); + } + return state; + } + + private settle(id: string) { + const entry = this.pending.get(id); + if (!entry) return; + clearTimeout(entry.timer); + this.pending.delete(id); + } + + private transmit(entry: PendingEntry) { + if (this.socket.readyState === WebSocket.OPEN) { + entry.attempts += 1; + this.socket.send(JSON.stringify(entry.message)); + } + entry.timer = setTimeout( + () => this.handleAckTimeout(entry), + ACK_TIMEOUT_MS, + ); + } + + private handleAckTimeout(entry: PendingEntry) { + if (!this.pending.has(entry.message.id)) return; + if (this.socket.readyState !== WebSocket.OPEN) { + // connection is down: hold the action for replay after the reconnect, + // which arrives via handleRoomstate + return; + } + if (entry.attempts >= MAX_SEND_ATTEMPTS) { + this.pending.delete(entry.message.id); + if (this.lastSeq != null && this.confirmed) { + // roll the abandoned change back out of the display state + this.handlers.applyState(this.rebase()); + } + this.handlers.onGiveUp(entry.message.action); + return; + } + this.transmit(entry); + } +} diff --git a/src/party/types.ts b/src/party/types.ts index 9bd43424a..cf0f134a0 100644 --- a/src/party/types.ts +++ b/src/party/types.ts @@ -9,6 +9,8 @@ export interface Roomstate { * pending re-sends whose effects are already baked into `state` */ recentActionIds?: string[]; + /** the seq of the last action baked into `state` */ + seq?: number; } export interface ReduxAction { @@ -16,6 +18,12 @@ export interface ReduxAction { action: Action; /** unique message id; when present the server confirms receipt with an ack */ id?: string; + /** + * canonical order assigned by the server. Present on server broadcasts, + * never on client sends. The server echoes a stamped copy of each action + * back to its sender as the receipt confirmation. + */ + seq?: number; } /** the server confirms it received and applied the action with this id */ diff --git a/src/state/event.slice.ts b/src/state/event.slice.ts index 57d4844e4..21560a070 100644 --- a/src/state/event.slice.ts +++ b/src/state/event.slice.ts @@ -37,13 +37,20 @@ export const eventSlice = createSlice({ initialState, reducers: { /** add a cab with its name */ - addCab(state, action: PayloadAction) { - const newCab: CabInfo = { - id: nanoid(5), - name: action.payload, - activeMatch: null, - }; - state.cabs[newCab.id] = newCab; + addCab: { + // the id must be minted here rather than in the reducer: actions + // replay on the party server and other clients, and every replica + // has to produce an identical cab + prepare(name: string) { + return { payload: { name, id: nanoid(5) } }; + }, + reducer(state, action: PayloadAction<{ name: string; id: string }>) { + state.cabs[action.payload.id] = { + id: action.payload.id, + name: action.payload.name, + activeMatch: null, + }; + }, }, removeCab(state, action: PayloadAction) { delete state.cabs[action.payload]; From 075a6c03f26862668f733f82dfc1b902a093ad99 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 15:44:52 +0000 Subject: [PATCH 161/168] Document the current event-mode backend and sync design Adds docs/partykit-sync-design.md: a developer-targeted tour of the partykit room server, wire protocol, and client replication model as they exist today, with mermaid sequence diagrams for the connect, happy-path, concurrent-edit rebase, retry/dedupe, and reconnect flows, and pointers to the source file implementing each piece. Cross-linked with the forward-looking roadmap doc. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AdFh3nxa4T3SdnYgGkDizY --- docs/partykit-sync-design.md | 306 ++++++++++++++++++++++++++++++++++ docs/partykit-sync-roadmap.md | 2 + 2 files changed, 308 insertions(+) create mode 100644 docs/partykit-sync-design.md diff --git a/docs/partykit-sync-design.md b/docs/partykit-sync-design.md new file mode 100644 index 000000000..8333a356a --- /dev/null +++ b/docs/partykit-sync-design.md @@ -0,0 +1,306 @@ +# Event-mode sync: how it works + +A developer's tour of the backend and state-sync design behind event mode +(`/e/:roomName`). This describes what exists today and where each piece +lives in the source. For where the design is headed next, see +[partykit-sync-roadmap.md](./partykit-sync-roadmap.md). + +## The big picture + +Every event is a [PartyKit](https://docs.partykit.io/) **room**: a single +server-side actor that owns the authoritative copy of that event's state and +relays updates between everyone viewing it. Both the browser and the room run +the **same redux reducer bundle** (`src/state/root-reducer.ts`), so state is +synchronized by shipping _redux actions_ over a websocket rather than +diffing state objects. The server orders those actions and every replica +applies them in the same order — a replicated state machine with a central +sequencer. + +```mermaid +flowchart LR + subgraph B1["Browser (organizer, spectator, OBS source, ...)"] + UI["React UI"] -->|dispatch| Store["redux store
(display state)"] + Gate["partyGateMiddleware"] --- Store + Store -->|listener middleware| SM["SyncManager
(confirmed + pending)"] + SM <-->|JSON messages| WS["PartySocket
(auto-reconnecting websocket)"] + end + WS <--> Room + subgraph PK["PartyKit room (one per event)"] + Room["Server class
src/party/server.ts"] --> SStore["server redux store"] + Room --> KV["room storage
('currentState')"] + Room --> SB[("Supabase
event_state table")] + end + B2["other clients"] <--> Room +``` + +Key source files: + +| Concern | File | +| ----------------------------------------------------- | ------------------------------------ | +| Room server (message handling, ordering, persistence) | `src/party/server.ts` | +| Wire protocol message types | `src/party/types.ts` | +| React socket manager (connect UI, toasts, wiring) | `src/party/client.tsx` | +| Client replication logic (confirmed/pending, retries) | `src/party/sync-manager.ts` | +| Dispatch gate while disconnected | `src/state/party-gate-middleware.ts` | +| Connection-health flag shared with middleware | `src/party/connection-status.ts` | +| Shared reducer + wholesale state replacement | `src/state/root-reducer.ts` | +| Host/endpoint resolution, HTTP state fetch | `src/party/host.ts` | +| Store construction and middleware order | `src/state/store.ts` | + +`PartySocketManager` (`src/party/client.tsx`) is mounted by the shells that +need a live room: the tournament UI (`src/tournament-mode/index.tsx`) and OBS +browser sources (`ObsSource` in `src/app.tsx`). Preview mode +(`src/preview-mode/shell.tsx`) is read-only and instead fetches a one-shot +state snapshot over HTTP (`getPartykitState` in `src/party/host.ts`, served +by `onRequest` in `src/party/server.ts`). Classic mode uses the same store +factory but never connects a socket. + +## Wire protocol + +All messages are JSON. The types live in `src/party/types.ts`. + +| Message | Direction | Meaning | +| ------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `{type:"action", action, id}` | client → server | "Please apply this redux action." `id` is a unique message id (nanoid) used for receipt confirmation and dedupe. | +| `{type:"action", action, id, seq}` | server → **all** clients | The same action, stamped with its canonical position. The copy sent back to the sender doubles as the receipt confirmation. | +| `{type:"roomstate", state, seq, recentActionIds}` | server → one client | Full state snapshot, sent on every (re)connect. `seq` is the last action baked into `state`; `recentActionIds` lets a reconnecting client drop pending re-sends that already landed. | +| `{type:"ack", id}` | server → sender | Sent instead of a re-apply when a duplicate re-send arrives for an already-applied `id`. | + +`id` and `seq` are optional on the wire for compatibility with older +deployments; see "Version interop" at the end. + +## The client state model + +The heart of the client is one invariant, maintained by `SyncManager` +(`src/party/sync-manager.ts`): + +> **display state == confirmed state + pending actions, replayed in order** + +- **Confirmed state** is built _only_ from server-stamped actions applied in + `seq` order. It converges identically on every replica. It lives as a + plain object inside `SyncManager` — not in the redux store. +- **Pending actions** are the user's own dispatches that the server hasn't + confirmed yet. They're already applied to the display store (optimistic + UI) and are re-sent until confirmed. +- **Display state** is what the redux store holds and the UI renders. + +Most of the time the invariant maintains itself with no extra work (your own +echo just moves the boundary between confirmed and pending). When it can't — +a foreign action gets ordered underneath your unconfirmed one, a reconnect +brings a fresh snapshot, or a pending action is abandoned — `SyncManager` +recomputes `confirmed + pending` with the shared reducer and swaps it into +the store wholesale. That swap is the `receivePartyState` action +(`src/state/central.ts`), which the root reducer treats as full state +replacement (`src/state/root-reducer.ts`). + +How a local dispatch reaches the wire: the redux listener middleware +(`src/state/listener-middleware.ts`, registered in `src/party/client.tsx`) +forwards every locally-originated action to `SyncManager.send()`. Actions +that arrived _from_ the network are marked `meta.source: "partykit"` and +skipped by the listener's predicate, so nothing echoes back out. + +## Message flows + +### Connecting + +```mermaid +sequenceDiagram + participant C as PartySocketManager (client.tsx) + participant S as Server (server.ts) + + C->>S: websocket connect + Note over S: onStart (once per room):
load state from room storage,
else Supabase, else fresh,
then applyMigrations + S->>C: {type:"roomstate", state, seq, recentActionIds} + Note over C: applyMigrations(state)
SyncManager adopts it as confirmed
dispatch(receivePartyState(...))
un-gate dispatch, render children +``` + +Until the first roomstate arrives, the UI shows a "Connecting..." card and +the app's children aren't rendered at all (`ready` state in +`src/party/client.tsx`). + +### The happy path: one client edits, everyone converges + +```mermaid +sequenceDiagram + actor U as User + participant D as redux store (display) + participant M as SyncManager + participant S as Server + participant O as Other client + + U->>D: dispatch(addCab("Left")) + Note over D: reducer applies optimistically + D->>M: listener middleware forwards action + Note over M: pending += m1, 5s ack timer + M->>S: {type:"action", action, id:"m1"} + Note over S: seq := 42
dispatch to server store
persist storage + Supabase + par echo = receipt confirmation + S->>M: {type:"action", action, id:"m1", seq:42} + Note over M: confirmed += action
m1 settled — display already right,
no re-dispatch needed + and relay + S->>O: {type:"action", action, id:"m1", seq:42} + Note over O: confirmed += action
no local pending → apply directly + end +``` + +Server-side, `onMessage` in `src/party/server.ts` does the stamping, +broadcasts to **everyone including the sender**, applies the action to its +own store, writes the new state to room storage (`currentState` key), and +upserts it to Supabase's `event_state` table when credentials are configured. + +### Concurrent edits: the rebase + +This is why confirmed/pending exists. Suppose client A dispatches `AA` while +client B's `BB` is in flight, and the server orders `BB` first: + +```mermaid +sequenceDiagram + participant A as Client A (SyncManager) + participant S as Server + participant B as Client B + + B->>S: action BB (id:"b1") + A->>S: action AA (id:"a1") + Note over A: display = confirmed + [AA] + S-->>B: BB stamped seq:7 + S->>A: BB stamped seq:7 + Note over A: foreign action under a pending one:
confirmed += BB
REBASE: dispatch receivePartyState(confirmed + [AA]) + S-->>B: AA stamped seq:8 + S->>A: AA stamped seq:8 + Note over A: own echo at head of pending:
confirmed += AA, a1 settled, no dispatch +``` + +Both clients end at `confirmed = ...BB, AA` — identical state in canonical +order, even though A briefly displayed `AA` without `BB`. Without the +sequencer each replica would have kept its own arrival order forever. + +This only works because reducers are **deterministic**: replaying the same +action must produce the same state on every replica. Ids and timestamps must +be minted in `prepare` callbacks or thunks so they ride inside the action +payload (see `addCab` in `src/state/event.slice.ts` for the pattern), never +inside reducers. + +### Loss, retry, and giving up + +Sends are tracked until the stamped echo (or an `ack`) settles them +(`transmit`/`handleAckTimeout` in `src/party/sync-manager.ts`): + +```mermaid +sequenceDiagram + participant M as SyncManager + participant S as Server + + M->>S: {action, id:"m1"} (attempt 1) + Note over M: no confirmation for 5s + M->>S: {action, id:"m1"} (attempt 2, same id) + Note over S: first copy applied at seq:9,
duplicate recognized via seenActionIds + S->>M: {action, id:"m1", seq:9} + S->>M: {type:"ack", id:"m1"} + Note over M: settled once — duplicate ack is a no-op +``` + +- The server remembers the last 1000 applied ids (`seenActionIds` in + `src/party/server.ts`) so a re-send is **never applied twice** — it just + gets re-confirmed. +- After 4 unconfirmed attempts on a live connection, the client gives up: + the action is dropped from pending, the display state is rebased _without + it_ (rolling the phantom change back out of the UI), and a danger toast + explains that the change could not be delivered. + +### Disconnect and reconnect + +```mermaid +sequenceDiagram + actor U as User + participant C as Client + participant S as Server + + Note over C,S: socket dies (network drop, server restart, ...) + Note over C: onClose: gate dispatch shut,
persistent "Lost connection" toast + U--xC: edits are blocked (warning toast) + C->>S: reconnect (automatic, PartySocket) + S->>C: {type:"roomstate", state, seq, recentActionIds} + Note over C: pending ids in recentActionIds → drop
(already baked into the snapshot),
the rest → rebase onto snapshot + re-send + C->>S: re-send surviving pending actions + Note over C: un-gate dispatch,
"Reconnected" toast +``` + +Two details worth knowing: + +- **Dispatch stays gated until the roomstate is fully applied**, not merely + until the socket reopens. This closes a race where an action dispatched in + the open-but-not-yet-synced window would be silently erased by the + arriving snapshot. The gate is `partyGateMiddleware` + (`src/state/party-gate-middleware.ts`), which consults a module-level + health flag (`src/party/connection-status.ts`) that deliberately lives + _outside_ the synced `AppState` — connection health must never replicate + to other clients or persist to the server. +- Actions that went unconfirmed _before_ the drop survive it: they're held + in pending and replayed after the snapshot. `recentActionIds` exists so an + action that actually landed just before the disconnect isn't applied a + second time. + +If a client ever observes a **gap in `seq`** (a missed broadcast on a live +socket), it can't trust its confirmed state anymore; the repair is to force +a reconnect and take a fresh roomstate (`resync` handler wired to +`socket.reconnect()` in `src/party/client.tsx`). + +## User-facing connection health + +All the toasts live in `src/party/client.tsx` (strings in +`src/assets/i18n.json` under `party.*`, en + ja): + +| Moment | UI | +| ------------------------------------- | ----------------------------------------- | +| Socket closed after a successful sync | persistent danger toast; edits blocked | +| Edit attempted while blocked | warning toast, action dropped by the gate | +| Roomstate applied after reconnect | success toast, edits re-enabled | +| Action abandoned after repeated sends | danger toast + local rollback | + +Toasts render through the app-root `OverlayToaster` (`src/toaster.tsx`) and +are suppressed when running inside an OBS browser source (`useInObs` from +`src/theme-toggle.tsx`) — an overlay on stream should never show connection +chrome. + +## Server persistence + +`src/party/server.ts` keeps three copies of truth in sync: + +1. **In-memory redux store** — authoritative while the room actor is alive. + Hydrated in `onStart` from room storage, falling back to Supabase, with + `applyMigrations` (`src/state/migrations.ts`) run over whatever loads. +2. **PartyKit room storage** — `currentState` key, rewritten after every + applied action. Survives room hibernation/restarts. +3. **Supabase** (`event_state` table, typed in + `src/party/database.types.ts`) — best-effort upsert after every action; + disabled unless `SUPABASE_URL`/`SUPABASE_KEY` are present. Serves as + cross-deployment durability. + +The room also answers plain HTTP `GET` with its current state JSON +(`onRequest`), which preview mode and debugging tools use via +`partykitEndpoint`/`getPartykitState` in `src/party/host.ts`. In development +the client targets `localhost:1999` (`yarn start:backend`); in production, +the deployed PartyKit host — both resolved in `src/party/host.ts`. + +## Version interop + +The protocol fields are optional so mixed deployments degrade instead of +corrupting state: + +- Actions **without `id`** (pre-protocol clients) are relayed the old way — + broadcast excluding the sender, unstamped — because such clients can't + recognize their own echo and would double-apply it. +- A roomstate **without `seq`** (older server) drops the client back to + plain optimistic apply: pending tracking still works via `ack`s, but no + rebasing and no give-up rollback (guarded by `lastSeq == null` in + `src/party/sync-manager.ts`). +- Deploy order when the protocol grows: **server first, web app second.** + +## Poking at it locally + +Run `yarn start:backend` (PartyKit on `:1999`) and `yarn start:frontend` +(webpack on `:8080`), then open `/e/any-room-name` in two tabs and watch the +websocket frames in devtools. `.claude/skills/verify/SKILL.md` documents a +scripted version of this, including how to freeze the server mid-flight +(SIGSTOP the `workerd` process) to exercise the retry, rebase, and +reconnect paths. diff --git a/docs/partykit-sync-roadmap.md b/docs/partykit-sync-roadmap.md index d0c4779b1..40c8f3773 100644 --- a/docs/partykit-sync-roadmap.md +++ b/docs/partykit-sync-roadmap.md @@ -4,6 +4,8 @@ This documents the design plan for evolving the partykit-based sync behind event mode (`/e/:roomName`) into something robust enough for flaky venue wifi and concurrent editing, **without abandoning redux**. It was written alongside PR #604 so the work can be picked up later from a fresh context. +For a guided tour of the implementation as it exists today, see +[partykit-sync-design.md](./partykit-sync-design.md). ## Where this is going, in one paragraph From 0688aebcfac97d103102e605f4f4a07afb97e95e Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 8 Jul 2026 11:34:31 -0700 Subject: [PATCH 162/168] remove redundant i18n provider --- src/app.tsx | 12 +++++------- src/intl-provider.tsx | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index fbaefa1bb..3b1afc78a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -10,7 +10,7 @@ import { FocusStyleManager } from "@blueprintjs/core"; FocusStyleManager.onlyShowFocusOnTabs(); import { UpdateManager } from "./update-manager"; -import { IntlProvider } from "./intl-provider"; +import { IntlProviderWrapper } from "./intl-provider"; import { ThemeSyncWidget } from "./theme-toggle"; import { Provider } from "react-redux"; import { createClientStore, useAppState } from "./state/store"; @@ -247,10 +247,8 @@ function ObsSource() { return ( - - - - + + ); @@ -263,11 +261,11 @@ function ObsStyles() { export function App() { return ( - + - + ); } diff --git a/src/intl-provider.tsx b/src/intl-provider.tsx index 656d52565..84609c7c2 100644 --- a/src/intl-provider.tsx +++ b/src/intl-provider.tsx @@ -10,7 +10,7 @@ const FALLBACK_LOCALE = "en"; const typedTranslations = translations as Record; -export function IntlProvider({ children }: { children: ReactNode }) { +export function IntlProviderWrapper({ children }: { children: ReactNode }) { const allLoadedData = useAtomValue(stockDataCache); const messages = useMemo(() => { From fb11a79358ad5ddf8da465e7d3dce94873492868 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 8 Jul 2026 11:34:45 -0700 Subject: [PATCH 163/168] add a few extra doc comments to sync manager --- src/party/sync-manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/party/sync-manager.ts b/src/party/sync-manager.ts index dde33512e..4a5737b39 100644 --- a/src/party/sync-manager.ts +++ b/src/party/sync-manager.ts @@ -14,6 +14,7 @@ interface SocketLike { send(data: string): void; } +/** An action taken locally that hasn't yet been confirmed by the server */ interface PendingEntry { message: ReduxAction & { id: string }; attempts: number; @@ -169,6 +170,9 @@ export class SyncManager { return state; } + /** + * removes a pending action from our list of pending as the server has now confirmed it + */ private settle(id: string) { const entry = this.pending.get(id); if (!entry) return; From 690ae2d326686c23ecc90a78248a5c933416d8b1 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 8 Jul 2026 11:34:55 -0700 Subject: [PATCH 164/168] tweak disconnected message --- src/assets/i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n.json b/src/assets/i18n.json index 8e43247f3..19141ba7e 100644 --- a/src/assets/i18n.json +++ b/src/assets/i18n.json @@ -108,7 +108,7 @@ "updateReady": "Update ready, refresh for the freshest code around!", "applyUpdate": "Apply", "party": { - "disconnected": "Lost connection to the event server. Changes are disabled until it reconnects.", + "disconnected": "Lost connection to the event server. Changes are disabled until reconnected.", "reconnected": "Reconnected to the event server!", "actionBlocked": "Still reconnecting to the event server. Your change was not applied.", "sendFailed": "A change could not be delivered to the event server after several attempts." From d854a1158385e4a96463e9bcf3bc1b861f98b0db Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Wed, 8 Jul 2026 11:38:43 -0700 Subject: [PATCH 165/168] bump partysocket --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c07de4d5f..82f14a0f3 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "p-queue": "9.0.1", "papaparse": "5.5.3", "partykit": "0.0.115", - "partysocket": "1.1.19", + "partysocket": "1.3.0", "postcss": "8.5.6", "postcss-loader": "8.2.0", "react": "19.2.3", diff --git a/yarn.lock b/yarn.lock index 5a8974b3f..8ad653df1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6577,7 +6577,7 @@ __metadata: p-queue: "npm:9.0.1" papaparse: "npm:5.5.3" partykit: "npm:0.0.115" - partysocket: "npm:1.1.19" + partysocket: "npm:1.3.0" postcss: "npm:8.5.6" postcss-loader: "npm:8.2.0" react: "npm:19.2.3" @@ -10987,9 +10987,9 @@ __metadata: languageName: node linkType: hard -"partysocket@npm:1.1.19": - version: 1.1.19 - resolution: "partysocket@npm:1.1.19" +"partysocket@npm:1.3.0": + version: 1.3.0 + resolution: "partysocket@npm:1.3.0" dependencies: event-target-polyfill: "npm:^0.0.4" peerDependencies: @@ -10997,7 +10997,7 @@ __metadata: peerDependenciesMeta: react: optional: true - checksum: 10/e563c9b24ff6e62726f960d2854138db8a87371763085dd2be23a9c42f5887dced188b42d3d8f4c6966da4ae341040595ad7f57141f09101f5c1cc5cff5d79e4 + checksum: 10/9e5566a87377b4337b2554d55c2a6586b5810bbf3db53de2196fc3f8132fe86253e2d3d16f90237f377f9ec24bde59b511798e80e3a5bfde12c2604d006684fe languageName: node linkType: hard From 831aaf0e2f56a26e81fd57cd8898904fd6ff2466 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 11 Jul 2026 21:43:28 -0700 Subject: [PATCH 166/168] convert player list to string id-based, add easier reordering --- package.json | 5 +- src/controls/draw-dialog.tsx | 16 +-- src/controls/player-list-input.tsx | 128 +++++++++++++++++ src/models/Drawing.ts | 84 +++++------ src/obs-sources/text.tsx | 9 +- src/preview-mode/header.tsx | 3 +- src/song-card/acton-menu.tsx | 39 +++-- src/song-card/card-label.tsx | 8 +- src/song-card/song-card.tsx | 30 ++-- src/song-card/use-player-label.ts | 6 +- src/state/drawings.slice.ts | 184 ++++++++++++++++++++++-- src/state/migrations.ts | 7 +- src/state/thunks.ts | 8 +- src/tournament-mode/cab-management.tsx | 6 +- src/tournament-mode/drawing-actions.tsx | 33 ++--- src/tournament-mode/drawing-labels.tsx | 30 ++-- src/tournament-mode/gauntlet-scores.tsx | 4 +- yarn.lock | 11 ++ 18 files changed, 448 insertions(+), 163 deletions(-) create mode 100644 src/controls/player-list-input.tsx diff --git a/package.json b/package.json index 82f14a0f3..274916cf2 100644 --- a/package.json +++ b/package.json @@ -142,5 +142,8 @@ "engines": { "node": "^24.0.0" }, - "packageManager": "yarn@4.12.0" + "packageManager": "yarn@4.12.0", + "dependencies": { + "react-movable": "^3.4.1" + } } diff --git a/src/controls/draw-dialog.tsx b/src/controls/draw-dialog.tsx index 6eadf9080..bac4f07fe 100644 --- a/src/controls/draw-dialog.tsx +++ b/src/controls/draw-dialog.tsx @@ -4,15 +4,15 @@ import { Tabs, Tab, InputGroup, - TagInput, Button, } from "@blueprintjs/core"; import { ConfigSelect } from "."; +import { PlayerListInput } from "./player-list-input"; import { MatchPicker, GauntletPicker, PickedMatch } from "../matches"; import { StartggApiKeyGated } from "../startgg-gql/components"; import { createDraw } from "../state/thunks"; import { useAppDispatch } from "../state/store"; -import { SimpleMeta } from "../models/Drawing"; +import { Player, SimpleMeta, newPlayer } from "../models/Drawing"; import { useState } from "react"; import { useAppMode } from "../common-components/app-mode"; import { DrawingMeta } from "../card-draw"; @@ -34,7 +34,7 @@ export function DrawDialog(props: Props) { return handleDraw({ type: "startgg", subtype: match.subtype, - entrants: match.players, + players: match.players, title: match.title, id: match.id, phaseName: match.phaseName, @@ -100,8 +100,9 @@ export function CustomDrawForm(props: { submitText?: string; onSubmit(meta: SimpleMeta): void; }) { - const [players, setPlayers] = useState( - props.initialMeta?.players || ["P1", "P2"], + // meta.players is already in display order + const [players, setPlayers] = useState( + () => props.initialMeta?.players ?? [newPlayer("P1"), newPlayer("P2")], ); const [title, setTitle] = useState(props.initialMeta?.title || ""); @@ -121,10 +122,7 @@ export function CustomDrawForm(props: { /> - setPlayers(v as string[])} - values={players} - /> + + + ); +} diff --git a/src/models/Drawing.ts b/src/models/Drawing.ts index 0b62a11ca..b6fba4be7 100644 --- a/src/models/Drawing.ts +++ b/src/models/Drawing.ts @@ -1,3 +1,4 @@ +import { nanoid } from "nanoid"; import { Song } from "./SongData"; export interface EligibleChart { @@ -34,8 +35,19 @@ export interface DrawnChart extends EligibleChart { type: typeof CHART_DRAWN; } +export interface Player { + id: string; + name: string; +} + +/** create a new player with a freshly-generated unique id */ +export function newPlayer(name: string): Player { + return { id: nanoid(10), name }; +} + export interface PlayerActionOnChart { - player: number; + /** id of the player who took the action */ + player: string; chartId: string; } @@ -43,11 +55,14 @@ export interface PocketPick extends PlayerActionOnChart { pick: EligibleChart; } -interface StartggMeta { - type: "startgg"; +interface DrawMeta { title: string; + players: Player[]; +} + +interface StartggMeta extends DrawMeta { + type: "startgg"; phaseName: string; - entrants: Array<{ id: string; name: string }>; } export interface StartggVersusMeta extends StartggMeta { @@ -64,49 +79,37 @@ export interface StartggGauntletMeta extends StartggMeta { scoresByEntrant?: Record>; } -export interface SimpleMeta { +export interface SimpleMeta extends DrawMeta { type: "simple"; - title: string; - /** plain player names */ - players: string[]; } -export function playerCount(meta: Drawing["meta"]) { - switch (meta.type) { - case "simple": - return meta.players.length; - case "startgg": - return meta.entrants.length; - } +/** a player's name, falling back to a positional placeholder when unnamed */ +export function playerDisplayName(player: Player, index: number) { + return player.name || `P${index + 1}`; } -export function getAllPlayers(d: Pick) { - const ret = [] as string[]; - for (let i = 1; i <= playerCount(d.meta); i++) { - ret.push(playerNameByDisplayPos(d, i)); - } - return ret; +export function getAllPlayers(d: Pick) { + return d.meta.players.map(playerDisplayName); } -export function playerNameByDisplayPos( - d: Pick, - pos: number, -) { - const playerIndex = d.playerDisplayOrder[pos - 1]; - return playerNameByIndex(d.meta, playerIndex); +export function playerById(meta: Drawing["meta"], id: string) { + return meta.players.find((p) => p.id === id); } -export function playerNameByIndex( +/** + * Display name for a player id. A present-but-unnamed player falls back to its + * positional placeholder (`P1`, `P2`, …); an id matching no player yields the + * `fallback` (empty by default). + */ +export function playerNameById( meta: Drawing["meta"], - idx: number, - fallback = `P${idx + 1}`, + id: string, + fallback = "", ) { - switch (meta.type) { - case "simple": - return meta.players[idx] || fallback; - case "startgg": - return meta.entrants[idx].name || fallback; - } + const index = meta.players.findIndex((p) => p.id === id); + return index === -1 + ? fallback + : playerDisplayName(meta.players[index], index); } /** used to reference a sub draw, or the charts in the parent draw by omitting the target */ @@ -116,16 +119,15 @@ export interface Drawing { id: string; configId: string; meta: SimpleMeta | StartggVersusMeta | StartggGauntletMeta; - /** index of items of the players array, in the order they should be displayed */ - playerDisplayOrder: number[]; - /** map of song ID to player index */ - winners: Record; + /** map of song ID to the id of the winning player */ + winners: Record; /** @deprecated migrating to subDraws */ charts?: Array; bans: Record; protects: Record; pocketPicks: Record; - priorityPlayer?: number; + /** id of the player who currently has priority, if any */ + priorityPlayer?: string; subDrawings: Record; } diff --git a/src/obs-sources/text.tsx b/src/obs-sources/text.tsx index efda0903c..3ba54f0cc 100644 --- a/src/obs-sources/text.tsx +++ b/src/obs-sources/text.tsx @@ -1,7 +1,7 @@ import { useParams } from "react-router-dom"; import { drawingsSlice } from "../state/drawings.slice"; import { useAppState } from "../state/store"; -import { getAllPlayers, playerNameByIndex } from "../models/Drawing"; +import { getAllPlayers } from "../models/Drawing"; export function GlobalLabel() { const params = useParams<"roomName" | "labelId">(); @@ -53,15 +53,16 @@ export function CabPlayer(props: { if (!drawingId) return null; const [parent] = drawingsSlice.selectors.byCompoundOrPlainId(s, drawingId); if (!parent) return null; - const playerIndex = parent.playerDisplayOrder[props.p - 1]; - const name = playerNameByIndex(parent.meta, playerIndex, ""); + const player = parent.meta.players[props.p - 1]; + const playerId = player?.id; + const name = player?.name || ""; const hideWins = parent.meta.type === "startgg" && parent.meta.subtype === "gauntlet"; if (hideWins) { return name; } const score = Object.values(parent.winners).reduce((prev, curr) => { - if (curr === playerIndex) return prev + 1; + if (curr === playerId) return prev + 1; return prev; }, 0); if (displayType === "name") { diff --git a/src/preview-mode/header.tsx b/src/preview-mode/header.tsx index 667f3324f..4c42c664e 100644 --- a/src/preview-mode/header.tsx +++ b/src/preview-mode/header.tsx @@ -5,6 +5,7 @@ import { NewLayers } from "@blueprintjs/icons"; import { ConfigSelect } from "../controls"; import { useState } from "react"; import { createDraw } from "../state/thunks"; +import { newPlayer } from "../models/Drawing"; import { useAppDispatch } from "../state/store"; import { useNavigate } from "react-router-dom"; @@ -37,7 +38,7 @@ function PreviewModeControls() { meta: { type: "simple", title: "Sample Draw", - players: ["P1", "P2"], + players: [newPlayer("P1"), newPlayer("P2")], }, }, selected!, diff --git a/src/song-card/acton-menu.tsx b/src/song-card/acton-menu.tsx index 08aec6dfe..a6ae124c6 100644 --- a/src/song-card/acton-menu.tsx +++ b/src/song-card/acton-menu.tsx @@ -11,15 +11,14 @@ import { } from "@blueprintjs/icons"; import { Menu, MenuItem, MenuDivider } from "@blueprintjs/core"; import { useDrawing } from "../drawing-context"; -import { playerNameByIndex } from "../models/Drawing"; import { JSX } from "react"; interface Props { - onStartPocketPick?: (p: number) => void; - onVeto?: (p: number) => void; - onProtect?: (p: number) => void; + onStartPocketPick?: (p: string) => void; + onVeto?: (p: string) => void; + onProtect?: (p: string) => void; onRedraw?: () => void; - onSetWinner?: (p: number | null) => void; + onSetWinner?: (p: string | null) => void; onCopy?: () => void; } @@ -87,19 +86,16 @@ export function ActionMenu(props: Props) { } export function FillPlaceholderList(props: { - onFillPlaceholder(p: number): void; + onFillPlaceholder(p: string): void; }) { - const drawingMeta = useDrawing((d) => d.meta); - const players = useDrawing((d) => d.playerDisplayOrder).map( - (pIdx) => [playerNameByIndex(drawingMeta, pIdx), pIdx] as const, - ); + const players = useDrawing((d) => d.meta.players); return ( - {players.map(([playerName, pIdx]) => ( + {players.map((player) => ( props.onFillPlaceholder(pIdx)} + key={player.id} + text={`Pick as ${player.name}`} + onClick={() => props.onFillPlaceholder(player.id)} icon={} /> ))} @@ -110,21 +106,18 @@ export function FillPlaceholderList(props: { interface IconRowProps { icon: JSX.Element; text: string; - onClick: (p: number) => void; + onClick: (p: string) => void; } function PlayerList({ icon, text, onClick }: IconRowProps) { - const drawingMeta = useDrawing((d) => d.meta); - const players = useDrawing((d) => d.playerDisplayOrder).map( - (pIdx) => [playerNameByIndex(drawingMeta, pIdx), pIdx] as const, - ); + const players = useDrawing((d) => d.meta.players); return ( - {players.map(([playerName, pIdx]) => ( + {players.map((player) => ( onClick(pIdx)} + key={player.id} + text={player.name} + onClick={() => onClick(player.id)} icon={} /> ))} diff --git a/src/song-card/card-label.tsx b/src/song-card/card-label.tsx index f09433bfe..1cb799795 100644 --- a/src/song-card/card-label.tsx +++ b/src/song-card/card-label.tsx @@ -10,7 +10,7 @@ import { Draw, SVGIconProps, } from "@blueprintjs/icons"; -import { usePlayerLabelForIndex } from "./use-player-label"; +import { usePlayerLabelForId } from "./use-player-label"; export enum LabelType { Protect = 1, @@ -21,7 +21,7 @@ export enum LabelType { } interface Props { - playerIdx: number; + playerId: string; type: LabelType; onRemove?: () => void; } @@ -56,8 +56,8 @@ function LabelIcon({ type, ...props }: SVGIconProps & { type: LabelType }) { } } -export function CardLabel({ playerIdx, type, onRemove }: Props) { - const label = usePlayerLabelForIndex(playerIdx); +export function CardLabel({ playerId, type, onRemove }: Props) { + const label = usePlayerLabelForId(playerId); const rootClassname = classNames(styles.cardLabel, { [styles.winner]: type === LabelType.Winner, diff --git a/src/song-card/song-card.tsx b/src/song-card/song-card.tsx index 95c6f7516..b7c479ab6 100644 --- a/src/song-card/song-card.tsx +++ b/src/song-card/song-card.tsx @@ -29,24 +29,24 @@ import { useChartRandomSelected } from "../tournament-mode/highlight-random"; import { baseChartValues, CardContentsProps } from "./variants"; -type PlayerIdx = number; +type PlayerId = string; interface IconCallbacks { - onVeto: (p: PlayerIdx) => void; - onProtect: (p: PlayerIdx) => void; - onReplace: (p: PlayerIdx, chart: EligibleChart) => void; + onVeto: (p: PlayerId) => void; + onProtect: (p: PlayerId) => void; + onReplace: (p: PlayerId, chart: EligibleChart) => void; onRedraw: () => void; onReset: () => void; - onSetWinner: (p: PlayerIdx | null) => void; + onSetWinner: (p: PlayerId | null) => void; } export interface SongCardProps { onClick?: () => void; chart: DrawnChart | EligibleChart | PlayerPickPlaceholder; - vetoedBy?: PlayerIdx; - protectedBy?: PlayerIdx; - replacedBy?: PlayerIdx; - winner?: PlayerIdx | null; + vetoedBy?: PlayerId; + protectedBy?: PlayerId; + replacedBy?: PlayerId; + winner?: PlayerId | null; replacedWith?: EligibleChart; actionsEnabled?: boolean; } @@ -62,7 +62,7 @@ function useIconCallbacksForChart(chartId: string): IconCallbacks { const handleBanPickPocket = useCallback( ( type: "ban" | "protect" | "pocket", - player: number, + player: string, pick?: EligibleChart, ) => dispatch(createPickBanPocket(drawingId, chartId, type, player, pick)), [drawingId, chartId, dispatch], @@ -118,7 +118,7 @@ export function SongCardBase(props: Props) { const hideMenu = () => setContextMenuOpen(false); const [pocketPickPendingForPlayer, setPocketPickPendingForPlayer] = - useState(null); + useState(null); const baseChartIsPlaceholder = "type" in chart && chart.type === CHART_PLACEHOLDER; @@ -194,28 +194,28 @@ export function SongCardBase(props: Props) { <> {vetoedBy !== undefined && ( )} {protectedBy !== undefined && ( )} {replacedBy !== undefined && ( )} {winner !== undefined && winner !== null && ( iconCallbacks?.onSetWinner(null)} /> diff --git a/src/song-card/use-player-label.ts b/src/song-card/use-player-label.ts index b3ecd1f9d..dfd80b652 100644 --- a/src/song-card/use-player-label.ts +++ b/src/song-card/use-player-label.ts @@ -1,6 +1,6 @@ import { useDrawing } from "../drawing-context"; -import { playerNameByIndex } from "../models/Drawing"; +import { playerNameById } from "../models/Drawing"; -export function usePlayerLabelForIndex(pIdx: number) { - return useDrawing((d) => playerNameByIndex(d.meta, pIdx)); +export function usePlayerLabelForId(playerId: string) { + return useDrawing((d) => playerNameById(d.meta, playerId)); } diff --git a/src/state/drawings.slice.ts b/src/state/drawings.slice.ts index 4c2122aab..ff8eacd06 100644 --- a/src/state/drawings.slice.ts +++ b/src/state/drawings.slice.ts @@ -12,6 +12,8 @@ import { DrawnChart, EligibleChart, MergedDrawing, + newPlayer, + Player, PlayerActionOnChart, SubDrawing, PlayerPickPlaceholder, @@ -31,7 +33,7 @@ type PlayerActionOnChartPayload = PayloadAction< { drawingId: CompoundSetId; chartId: string; - player: number; + player: string; reorder: boolean; } & extra >; @@ -91,13 +93,62 @@ export const drawingsSlice = createSlice({ } Object.assign(chart, action.payload.changes); }, + /** + * Persist edits from the "edit title & players" dialog (rename/reorder/ + * add/remove). Because players carry stable ids, reordering and renaming + * need no fixup; removing a player just means dropping the card actions + * (winners, bans, protects, pocket picks) that still reference its + * now-absent id. + */ + updatePlayers( + state, + action: PayloadAction<{ + id: string; + title: string; + players: Player[]; + }>, + ) { + const { id, title, players } = action.payload; + const drawing = state.entities[id]; + if (!drawing) { + return; + } + + const remainingIds = new Set(players.map((p) => p.id)); + + for (const [chartId, winner] of Object.entries(drawing.winners)) { + if (winner !== null && !remainingIds.has(winner)) { + delete drawing.winners[chartId]; + } + } + + for (const record of [ + drawing.bans, + drawing.protects, + drawing.pocketPicks, + ]) { + for (const [chartId, entry] of Object.entries(record)) { + if (entry && !remainingIds.has(entry.player)) { + delete record[chartId]; + } + } + } + + // drop the priority player if they were removed from the roster + if (drawing.priorityPlayer && !remainingIds.has(drawing.priorityPlayer)) { + drawing.priorityPlayer = undefined; + } + + drawing.meta.title = title; + drawing.meta.players = players; + }, swapPlayerPositions(state, action: ActionOnSingleDrawing) { const mainId = action.payload; const drawing = state.entities[mainId]; if (!drawing) { return; } - drawing.playerDisplayOrder = drawing.playerDisplayOrder.toReversed(); + drawing.meta.players.reverse(); }, incrementPriorityPlayer(state, action: ActionOnSingleDrawing) { const mainId = action.payload; @@ -105,16 +156,13 @@ export const drawingsSlice = createSlice({ if (!drawing) { return; } - let priorityPlayer = drawing.priorityPlayer; - if (!priorityPlayer) { - priorityPlayer = 1; - } else { - priorityPlayer += 1; - if (priorityPlayer >= drawing.playerDisplayOrder.length + 1) { - priorityPlayer = undefined; - } - } - drawing.priorityPlayer = priorityPlayer; + // cycle: nobody -> first player -> ... -> last player -> nobody + const players = drawing.meta.players; + const currentIndex = drawing.priorityPlayer + ? players.findIndex((p) => p.id === drawing.priorityPlayer) + : -1; + const next = players[currentIndex + 1]; + drawing.priorityPlayer = next?.id; }, resetChart(state, action: ActionOnSingleChart) { const { chartId, drawingId } = action.payload; @@ -178,7 +226,7 @@ export const drawingsSlice = createSlice({ }; } }, - setWinner(state, action: ActionOnSingleChart<{ player: number | null }>) { + setWinner(state, action: ActionOnSingleChart<{ player: string | null }>) { const [drawing] = getDrawingFromCompoundId( state, action.payload.drawingId, @@ -213,7 +261,7 @@ export const drawingsSlice = createSlice({ } if (!drawing.meta.scoresByEntrant) { drawing.meta.scoresByEntrant = {}; - for (const entrant of drawing.meta.entrants) { + for (const entrant of drawing.meta.players) { drawing.meta.scoresByEntrant[entrant.id] = {}; } } @@ -317,6 +365,114 @@ export function migrateToSubdraws(state: StateOfSlice) { } } +/** + * one-time migration from the old player model to the current one. The old + * model gave players plain string names (simple) or a `meta.entrants` array + * (startgg), kept display order in a separate `playerDisplayOrder` field, and + * referenced players by numeric index in `playerDisplayOrder`/`winners`/action + * `player`. The new model gives every player a stable id under `meta.players`, + * stores that array directly in display order, and references players by id. + * + * The presence of a `playerDisplayOrder` field marks a drawing still in an old + * shape, so this is idempotent (already-migrated drawings are skipped). mutates + * state. + */ +export function migratePlayersToIds(state: StateOfSlice) { + for (const id of state.ids) { + const drawing = state.entities[id]; + if (!drawing) { + continue; + } + // `playerDisplayOrder` no longer exists on the model; its presence means + // the drawing predates this migration. + const legacyDrawing = drawing as unknown as { + playerDisplayOrder?: Array; + priorityPlayer?: number | string; + }; + const legacyOrder = legacyDrawing.playerDisplayOrder; + if (!legacyOrder) { + continue; + } + + // startgg used to store players under `entrants` + const legacyMeta = drawing.meta as unknown as { + players?: Array; + entrants?: Player[]; + }; + const rawPlayers = legacyMeta.players ?? legacyMeta.entrants ?? []; + + // ensure every player is an object carrying a stable id (simple drawings + // used to store plain name strings) + const players: Player[] = rawPlayers.map((p) => + typeof p === "string" ? newPlayer(p) : p, + ); + + // legacy references were numeric indices into this (original-order) players + // array; normalize both those and any already-id references to a player id. + // A dangling index yields undefined, so its reference is dropped. + const idFor = (ref: number | string): string | undefined => + typeof ref === "number" ? players[ref]?.id : ref; + + // fold the separate display order into the players array order + const byId = new Map(players.map((p) => [p.id, p])); + const ordered: Player[] = []; + for (const ref of legacyOrder) { + const player = byId.get(idFor(ref)!); + if (player) { + ordered.push(player); + byId.delete(player.id); + } + } + // append any players not referenced by the display order (defensive) + ordered.push(...byId.values()); + + drawing.meta.players = ordered; + delete legacyMeta.entrants; + delete legacyDrawing.playerDisplayOrder; + + const legacyWinners = drawing.winners as Record< + string, + number | string | null + >; + for (const [chartId, val] of Object.entries(legacyWinners)) { + if (val === null) { + continue; + } + const winnerId = idFor(val); + if (winnerId === undefined) { + delete drawing.winners[chartId]; + } else { + drawing.winners[chartId] = winnerId; + } + } + + for (const record of [ + drawing.bans, + drawing.protects, + drawing.pocketPicks, + ]) { + for (const [chartId, entry] of Object.entries(record)) { + if (!entry) { + continue; + } + const action = entry as { player: number | string }; + const playerId = idFor(action.player); + if (playerId === undefined) { + delete record[chartId]; + } else { + action.player = playerId; + } + } + } + + // priorityPlayer was a 1-based display position; resolve it to a player id + const priority = legacyDrawing.priorityPlayer; + if (typeof priority === "number") { + drawing.priorityPlayer = ordered[priority - 1]?.id; + } + } +} + export function getDrawingFromCompoundId( state: StateOfSlice, id: CompoundSetId, diff --git a/src/state/migrations.ts b/src/state/migrations.ts index df6fec34f..157307412 100644 --- a/src/state/migrations.ts +++ b/src/state/migrations.ts @@ -1,9 +1,12 @@ -import { migrateToSubdraws } from "./drawings.slice"; +import { migratePlayersToIds, migrateToSubdraws } from "./drawings.slice"; import { addObsLabels } from "./event.slice"; import type { AppState } from "./root-reducer"; /** mutates `state` to apply any necessary migrations */ export function applyMigrations(state: AppState) { - if (state.drawings) migrateToSubdraws(state.drawings); + if (state.drawings) { + migrateToSubdraws(state.drawings); + migratePlayersToIds(state.drawings); + } if (state.event) addObsLabels(state.event); } diff --git a/src/state/thunks.ts b/src/state/thunks.ts index 8b922a246..c916f880c 100644 --- a/src/state/thunks.ts +++ b/src/state/thunks.ts @@ -59,11 +59,6 @@ export function createDraw( return "nok"; // could not draw the requested number of charts } - const players = - drawMeta.meta.type === "simple" - ? drawMeta.meta.players - : drawMeta.meta.entrants; - const matchId = `draw-${nanoid(10)}`; const setId = `set-${nanoid(12)}`; const mainDraw: SubDrawing = { @@ -78,7 +73,6 @@ export function createDraw( protects: {}, pocketPicks: {}, meta: drawMeta.meta, - playerDisplayOrder: players.map((_, idx) => idx), configId, subDrawings: { [setId]: mainDraw }, }; @@ -308,7 +302,7 @@ export function createPickBanPocket( drawingId: CompoundSetId, chartId: string, type: "ban" | "protect" | "pocket", - player: number, + player: string, pick?: EligibleChart, ): AppThunk { return (dispatch, getState) => { diff --git a/src/tournament-mode/cab-management.tsx b/src/tournament-mode/cab-management.tsx index c4798b975..700fc2424 100644 --- a/src/tournament-mode/cab-management.tsx +++ b/src/tournament-mode/cab-management.tsx @@ -31,8 +31,8 @@ import { import { detectedLanguage } from "../utils"; import { useSetAtom } from "jotai"; import { mainTabAtom } from "./main-view"; -import { playerNameByIndex } from "../models/Drawing"; import { drawingsSlice } from "../state/drawings.slice"; +import { playerDisplayName } from "../models/Drawing"; import { copyObsSource, routableCabSourcePath } from "./copy-obs-source"; import { useHref } from "react-router-dom"; @@ -236,9 +236,7 @@ function CurrentMatch(props: { cab: CabInfo }) { if (!drawing) { return

No match

; } - const filledPlayers = drawing.playerDisplayOrder.map((pIdx, idx) => - playerNameByIndex(drawing.meta, pIdx, `Player ${idx + 1}`), - ); + const filledPlayers = drawing.meta.players.map(playerDisplayName); const assignmentType = typeof props.cab.activeMatch === "string" ? "match" : "set"; return ( diff --git a/src/tournament-mode/drawing-actions.tsx b/src/tournament-mode/drawing-actions.tsx index ee7608ffd..c8286e8f8 100644 --- a/src/tournament-mode/drawing-actions.tsx +++ b/src/tournament-mode/drawing-actions.tsx @@ -34,7 +34,7 @@ import { useDrawing } from "../drawing-context"; import { CHART_DRAWN, CHART_PLACEHOLDER, - playerCount, + playerById, StartggGauntletMeta, } from "../models/Drawing"; import { @@ -65,7 +65,6 @@ import { EventModeGated } from "../common-components/app-mode"; import { useIntl } from "../hooks/useIntl"; import { ConfigContextProvider, useConfigId } from "../state/hooks"; import { CustomDrawForm } from "../controls/draw-dialog"; -import { times } from "../utils"; import { mergeDraws } from "../state/central"; import { useHighlightRandom } from "./highlight-random"; @@ -83,25 +82,25 @@ function getMatchResult( return; } const winsPerPlayer = new CountingSet(); - for (const [songId, pIdx] of Object.entries(parent.winners)) { - if (pIdx === null) { + for (const [songId, playerId] of Object.entries(parent.winners)) { + if (playerId === null) { continue; } - try { - const entrant = parent.meta.entrants[pIdx]; - gameData.push({ gameNum: gameData.length + 1, winnerId: entrant.id }); - winsPerPlayer.add(entrant.id); - } catch (e) { - console.warn(`failed to add game data for song ${songId}`, e); + const entrant = playerById(parent.meta, playerId); + if (!entrant) { + console.warn(`failed to add game data for song ${songId}`); + continue; } + gameData.push({ gameNum: gameData.length + 1, winnerId: entrant.id }); + winsPerPlayer.add(entrant.id); } let winnerId: string | undefined; const orderedByWins = Array.from(winsPerPlayer.valuesWithCount()).sort( (a, b) => b[1] - a[1], ); if ( - orderedByWins.length == 1 || - orderedByWins[0][1] > orderedByWins[1][1] + orderedByWins.length === 1 || + (orderedByWins.length > 1 && orderedByWins[0][1] > orderedByWins[1][1]) ) { // confirmed no tie for first place winnerId = orderedByWins[0][0]; @@ -402,7 +401,7 @@ function EditMatchMenu({ drawingId }: { drawingId: string }) { const [metaEditorOpen, setMetaEditorOpen] = useState(false); const drawingMeta = useAppState((s) => s.drawings.entities[drawingId].meta); const configId = useAppState((s) => s.drawings.entities[drawingId].configId); - const isTwoPlayers = playerCount(drawingMeta) === 2; + const isTwoPlayers = drawingMeta.players.length === 2; const showLabels = useAtomValue(showPlayerAndRoundLabels); let editPlayersDialog: JSX.Element | null; @@ -414,12 +413,10 @@ function EditMatchMenu({ drawingId }: { drawingId: string }) { submitText="Save" onSubmit={(meta) => { dispatch( - drawingsSlice.actions.updateOne({ + drawingsSlice.actions.updatePlayers({ id: drawingId, - changes: { - meta, - playerDisplayOrder: times(meta.players.length, (n) => n - 1), - }, + title: meta.title, + players: meta.players, }), ); setMetaEditorOpen(false); diff --git a/src/tournament-mode/drawing-labels.tsx b/src/tournament-mode/drawing-labels.tsx index c0b0dddbc..36da182b2 100644 --- a/src/tournament-mode/drawing-labels.tsx +++ b/src/tournament-mode/drawing-labels.tsx @@ -7,12 +7,11 @@ import { useAtomValue } from "jotai"; import { showPlayerAndRoundLabels } from "../config-state"; import { useAppDispatch } from "../state/store"; import { drawingsSlice } from "../state/drawings.slice"; -import { getAllPlayers } from "../models/Drawing"; import { CountingSet } from "../utils/counting-set"; +import { playerDisplayName } from "../models/Drawing"; export function MatchLabels() { const showLabels = useAtomValue(showPlayerAndRoundLabels); - const playerDisplayOrder = useDrawing((d) => d.playerDisplayOrder); const meta = useDrawing((d) => d.meta); const winners = useDrawing((d) => d.winners); if (!showLabels) { @@ -20,34 +19,32 @@ export function MatchLabels() { } const hideWins = meta.type === "startgg" && meta.subtype === "gauntlet"; - let winsPerPlayer: CountingSet | undefined; + let winsPerPlayer: CountingSet | undefined; if (!hideWins) { - winsPerPlayer = new CountingSet(); - for (const pIdx of Object.values(winners)) { - if (pIdx === null) { + winsPerPlayer = new CountingSet(); + for (const pId of Object.values(winners)) { + if (pId === null) { continue; } - winsPerPlayer.add(pIdx); + winsPerPlayer.add(pId); } } - const allPlayers = getAllPlayers({ meta, playerDisplayOrder }); - return (
{meta.title}
- {allPlayers.map((name, idx) => { + {meta.players.map((player, idx) => { const winCount = winsPerPlayer ? ( - <> ({winsPerPlayer.get(playerDisplayOrder[idx])}) + <> ({winsPerPlayer.get(player.id)}) ) : null; const ret = ( - {name} + {playerDisplayName(player, idx)} {winCount} ); - if (allPlayers.length === 2 && idx === 0) { + if (meta.players.length === 2 && idx === 0) { return ( {ret} @@ -70,13 +67,15 @@ function Versus() { [dispatch, parentId], ); const priorityPlayer = useDrawing((s) => s.priorityPlayer); + const players = useDrawing((s) => s.meta.players); return (
@@ -87,7 +86,8 @@ function Versus() { icon={ diff --git a/src/tournament-mode/gauntlet-scores.tsx b/src/tournament-mode/gauntlet-scores.tsx index d7f7c0fa2..503789820 100644 --- a/src/tournament-mode/gauntlet-scores.tsx +++ b/src/tournament-mode/gauntlet-scores.tsx @@ -27,10 +27,10 @@ export default function GauntletScoreEditor({ ); const dispatch = useDispatch(); const [playerOrderMap, setPlayerOrderMap] = useState( - meta.entrants.map((_, idx) => idx), + meta.players.map((_, idx) => idx), ); - const players = meta.entrants; + const players = meta.players; function updateScore(playerIdx: number, chartId: string, rawInput: string) { const playerId = players[playerIdx].id; diff --git a/yarn.lock b/yarn.lock index 8ad653df1..93f7c386a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6584,6 +6584,7 @@ __metadata: react-dom: "npm:19.2.3" react-error-boundary: "npm:6.0.1" react-intl: "npm:8.0.9" + react-movable: "npm:^3.4.1" react-redux: "npm:^9.2.0" react-refresh: "npm:0.18.0" react-router-dom: "npm:^6.24.1" @@ -11929,6 +11930,16 @@ __metadata: languageName: node linkType: hard +"react-movable@npm:^3.4.1": + version: 3.4.1 + resolution: "react-movable@npm:3.4.1" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10/f9737f7a50e995a866ed015075a8ee36fb5234ea5ac10d9631f8817c41ec5627e8082188722e5f4c257e7f4e8e5a8bd92ffba2d8aed0eb9854ae6dc473f91b5b + languageName: node + linkType: hard + "react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" From e87329fa46e6f1e0ee810fc8c3bc9c383a185231 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 11 Jul 2026 21:47:09 -0700 Subject: [PATCH 167/168] re-sort deps --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 274916cf2..c9e8d8a3b 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "react-dom": "19.2.3", "react-error-boundary": "6.0.1", "react-intl": "8.0.9", + "react-movable": "^3.4.1", "react-redux": "^9.2.0", "react-refresh": "0.18.0", "react-router-dom": "^6.24.1", @@ -142,8 +143,5 @@ "engines": { "node": "^24.0.0" }, - "packageManager": "yarn@4.12.0", - "dependencies": { - "react-movable": "^3.4.1" - } + "packageManager": "yarn@4.12.0" } From a87de82c692aba22ff5c29b457ae48647d700607 Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 11 Jul 2026 22:03:15 -0700 Subject: [PATCH 168/168] open up CORS on party server get method --- src/party/server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/party/server.ts b/src/party/server.ts index aca5fbb7d..2b23540b1 100644 --- a/src/party/server.ts +++ b/src/party/server.ts @@ -80,8 +80,10 @@ export default class Server implements Party.Server { onRequest(req: Party.Request): Response | Promise { if (req.method === "GET") { - return new Response(JSON.stringify(this.store.getState()), { - headers: { "Content-Type": "application/json" }, + return Response.json(this.store.getState(), { + headers: { + "Access-Control-Allow-Origin": "*", + }, }); }