From b6d4ce4f800bf191b4060f8e52be27cdc2fed8bb Mon Sep 17 00:00:00 2001 From: Quaternions Date: Fri, 6 Jun 2025 17:14:27 -0700 Subject: [PATCH 01/11] submissions-api: add external delete endpoints --- validation/api/src/context.rs | 4 ++++ validation/api/src/external.rs | 20 ++++++++++++++++++++ validation/api/src/types.rs | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/validation/api/src/context.rs b/validation/api/src/context.rs index 3264027..36476b3 100644 --- a/validation/api/src/context.rs +++ b/validation/api/src/context.rs @@ -44,4 +44,8 @@ impl Context{ .body(body) .send().await } + pub async fn delete(&self,url:impl reqwest::IntoUrl)->Result{ + self.client.delete(url) + .send().await + } } diff --git a/validation/api/src/external.rs b/validation/api/src/external.rs index 58f93fa..e5b9cb0 100644 --- a/validation/api/src/external.rs +++ b/validation/api/src/external.rs @@ -72,6 +72,16 @@ impl Context{ ).await.map_err(Error::Response)? .json().await.map_err(Error::ReqwestJson) } + pub async fn delete_script(&self,config:GetScriptRequest)->Result<(),Error>{ + let url_raw=format!("{}/scripts/{}",self.0.base_url,config.ScriptID.0); + let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?; + + response_ok( + self.0.delete(url).await.map_err(Error::Reqwest)? + ).await.map_err(Error::Response)?; + + Ok(()) + } pub async fn get_script_policies(&self,config:GetScriptPoliciesRequest<'_>)->Result,Error>{ let url_raw=format!("{}/script-policy",self.0.base_url); let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?; @@ -130,6 +140,16 @@ impl Context{ self.0.post(url,body).await.map_err(Error::Reqwest)? ).await.map_err(Error::Response)?; + Ok(()) + } + pub async fn delete_script_policy(&self,config:GetScriptPolicyRequest)->Result<(),Error>{ + let url_raw=format!("{}/script-policy/{}",self.0.base_url,config.ScriptPolicyID.0); + let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?; + + response_ok( + self.0.delete(url).await.map_err(Error::Reqwest)? + ).await.map_err(Error::Response)?; + Ok(()) } } diff --git a/validation/api/src/types.rs b/validation/api/src/types.rs index 1e4ffe2..1616ac5 100644 --- a/validation/api/src/types.rs +++ b/validation/api/src/types.rs @@ -173,6 +173,10 @@ pub enum Policy{ Replace=4, } +#[allow(nonstandard_style)] +pub struct GetScriptPolicyRequest{ + pub ScriptPolicyID:ScriptPolicyID, +} #[allow(nonstandard_style)] #[derive(Clone,Debug,serde::Serialize)] pub struct GetScriptPoliciesRequest<'a>{ -- 2.49.1 From 131dad7ae07b9a972e7573146cf9521feca8d362 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Fri, 6 Jun 2025 22:28:07 -0700 Subject: [PATCH 02/11] submissions-api: v0.7.2 script policy delete endpoints --- Cargo.lock | 2 +- validation/api/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96674a0..75bd957 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1889,7 +1889,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "submissions-api" -version = "0.7.1" +version = "0.7.2" dependencies = [ "reqwest", "serde", diff --git a/validation/api/Cargo.toml b/validation/api/Cargo.toml index 42321bc..e4080ef 100644 --- a/validation/api/Cargo.toml +++ b/validation/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "submissions-api" -version = "0.7.1" +version = "0.7.2" edition = "2021" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/maps-service" -- 2.49.1 From ebe37ad6a218f12d0c6c6e3b7813b0b42869a6c8 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Fri, 6 Jun 2025 22:29:35 -0700 Subject: [PATCH 03/11] update deps --- Cargo.lock | 273 ++++++++++++++++++------------------------ validation/Cargo.toml | 2 +- 2 files changed, 117 insertions(+), 158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96674a0..2d61e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,9 +68,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-nats" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23419d455dc57d3ae60a2f4278cf561fc74fe866e548e14d2b0ad3e1b8ca0b2" +checksum = "2cf0ae68ffe9ef362127a2223b42f57104edb20a50429f8c6e058912212884f7" dependencies = [ "base64 0.22.1", "bytes", @@ -126,7 +126,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -143,9 +143,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" @@ -155,9 +155,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "blake3" @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "byteorder" @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.22" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -403,9 +403,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -425,9 +425,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -684,11 +684,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -717,22 +716,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -808,9 +813,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -824,9 +829,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -882,6 +887,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.15" @@ -957,9 +972,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1043,13 +1058,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1125,11 +1140,11 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -1157,9 +1172,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.108" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -1169,9 +1184,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1179,15 +1194,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1261,9 +1276,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" @@ -1465,7 +1480,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -1499,9 +1514,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" dependencies = [ "base64 0.22.1", "bytes", @@ -1525,21 +1540,20 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -1599,7 +1613,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -1674,9 +1688,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -1705,7 +1719,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -1857,15 +1871,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1941,7 +1955,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -2032,9 +2046,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -2127,6 +2141,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2152,9 +2184,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -2163,9 +2195,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2378,15 +2410,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", - "windows-strings 0.4.0", + "windows-strings", ] [[package]] @@ -2419,38 +2451,29 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ + "windows-link", "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -2461,7 +2484,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2470,7 +2493,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2479,30 +2502,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2511,103 +2518,55 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] diff --git a/validation/Cargo.toml b/validation/Cargo.toml index 1025aaf..06c3a83 100644 --- a/validation/Cargo.toml +++ b/validation/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] submissions-api = { path = "api", features = ["internal"], default-features = false, registry = "strafesnet" } -async-nats = "0.40.0" +async-nats = "0.41.0" futures = "0.3.31" rbx_asset = { version = "0.4.5", registry = "strafesnet" } rbx_binary = "1.0.0" -- 2.49.1 From 84d2bfef203b440dd3dd2934e0e81d11065c53d9 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Sat, 7 Jun 2025 16:29:06 -0700 Subject: [PATCH 04/11] remove class_is_a --- validation/src/check.rs | 50 +++++++++++++++++++++----------------- validation/src/rbx_util.rs | 8 ------ 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index dc112a7..8c39abe 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -1,6 +1,6 @@ use std::collections::{HashSet,HashMap}; use crate::download::download_asset_version; -use crate::rbx_util::{class_is_a,get_mapinfo,get_root_instance,read_dom,ReadDomError,GameID,ParseGameIDError,MapInfo,GetRootInstanceError,StringValueError}; +use crate::rbx_util::{get_mapinfo,get_root_instance,read_dom,ReadDomError,GameID,ParseGameIDError,MapInfo,GetRootInstanceError,StringValueError}; use heck::{ToSnakeCase,ToTitleCase}; @@ -225,27 +225,33 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d // count objects (default count is 0) let mut counts=Counts::default(); - for instance in dom.descendants_of(model_instance.referent()){ - if class_is_a(instance.class.as_str(),"BasePart"){ - // Zones - match instance.name.parse(){ - Ok(ModeElement{zone:Zone::Start,mode_id})=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()), - Ok(ModeElement{zone:Zone::Finish,mode_id})=>counts.mode_finish_counts.entry(mode_id).or_default().push(instance.name.as_str()), - Ok(ModeElement{zone:Zone::Anticheat,mode_id})=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()), - Err(_)=>(), - } - // Spawns & Teleports - match instance.name.parse(){ - Ok(StageElement{behaviour:StageElementBehaviour::Teleport,stage_id})=>counts.teleport_counts.entry(stage_id).or_default().push(instance.name.as_str()), - Ok(StageElement{behaviour:StageElementBehaviour::Spawn,stage_id})=>*counts.spawn_counts.entry(stage_id).or_insert(0)+=1, - Err(_)=>(), - } - // Wormholes - match instance.name.parse(){ - Ok(WormholeElement{behaviour:WormholeBehaviour::In,wormhole_id})=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1, - Ok(WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id})=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1, - Err(_)=>(), - } + + let db=rbx_reflection_database::get(); + let base_part=&db.classes["BasePart"]; + let base_parts=dom.descendants_of(model_instance.referent()).filter(|&instance| + db.classes.get(instance.class.as_str()).is_some_and(|class| + db.has_superclass(class,base_part) + ) + ); + for instance in base_parts{ + // Zones + match instance.name.parse(){ + Ok(ModeElement{zone:Zone::Start,mode_id})=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()), + Ok(ModeElement{zone:Zone::Finish,mode_id})=>counts.mode_finish_counts.entry(mode_id).or_default().push(instance.name.as_str()), + Ok(ModeElement{zone:Zone::Anticheat,mode_id})=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()), + Err(_)=>(), + } + // Spawns & Teleports + match instance.name.parse(){ + Ok(StageElement{behaviour:StageElementBehaviour::Teleport,stage_id})=>counts.teleport_counts.entry(stage_id).or_default().push(instance.name.as_str()), + Ok(StageElement{behaviour:StageElementBehaviour::Spawn,stage_id})=>*counts.spawn_counts.entry(stage_id).or_insert(0)+=1, + Err(_)=>(), + } + // Wormholes + match instance.name.parse(){ + Ok(WormholeElement{behaviour:WormholeBehaviour::In,wormhole_id})=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1, + Ok(WormholeElement{behaviour:WormholeBehaviour::Out,wormhole_id})=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1, + Err(_)=>(), } } diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs index fdff4d5..59cff0d 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -28,14 +28,6 @@ pub fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{ rbx_dom_weak::ustr(s) } -pub fn class_is_a(class:&str,superclass:&str)->bool{ - let db=rbx_reflection_database::get(); - let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{ - return false; - }; - db.has_superclass(class,superclass) -} - fn find_first_child_name_and_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str,class:&str)->Option<&'a rbx_dom_weak::Instance>{ instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name&&inst.class==class) } -- 2.49.1 From ada8c322dacbae1073460f80d96ebaaf0b4adab9 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Sat, 7 Jun 2025 17:07:47 -0700 Subject: [PATCH 05/11] openapi: add descriptions to enum fields --- openapi.yaml | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 8b656e4..fa1736e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -114,6 +114,13 @@ paths: format: int32 minimum: 0 maximum: 4 + description: > + Sort order: + * `0` - Disabled + * `1` - DisplayNameAscending + * `2` - DisplayNameDescending + * `3` - DateAscending + * `4` - DateDescending responses: "200": description: Successful response @@ -178,6 +185,11 @@ paths: format: int32 minimum: 1 maximum: 5 + description: > + Game ID: + * `1` - Bhop + * `2` - Surf + * `5` - FlyTrials - name: Sort in: query schema: @@ -185,6 +197,13 @@ paths: format: int32 minimum: 0 maximum: 4 + description: > + Sort order: + * `0` - Disabled + * `1` - DisplayNameAscending + * `2` - DisplayNameDescending + * `3` - DateAscending + * `4` - DateDescending - name: Submitter in: query schema: @@ -210,6 +229,24 @@ paths: format: int32 minimum: 0 maximum: 9 + description: > + // Phase: Creation + * `0` - UnderConstruction + * `1` - ChangesRequested + + // Phase: Review + * `2` - Submitting + * `3` - Submitted + + // Phase: Testing + * `4` - AcceptedUnvalidated // pending script review, can re-trigger validation + * `5` - Validating + * `6` - Validated + * `7` - Uploading + + // Phase: Final MapfixStatus + * `8` - Uploaded // uploaded to the group, but pending release + * `9` - Rejected responses: "200": description: Successful response @@ -602,6 +639,11 @@ paths: format: int32 minimum: 1 maximum: 5 + description: > + Game ID: + * `1` - Bhop + * `2` - Surf + * `5` - FlyTrials - name: Sort in: query schema: @@ -609,6 +651,13 @@ paths: format: int32 minimum: 0 maximum: 4 + description: > + Sort order: + * `0` - Disabled + * `1` - DisplayNameAscending + * `2` - DisplayNameDescending + * `3` - DateAscending + * `4` - DateDescending - name: Submitter in: query schema: @@ -634,6 +683,25 @@ paths: format: int32 minimum: 0 maximum: 10 + description: > + // Phase: Creation + * `0` - UnderConstruction + * `1` - ChangesRequested + + // Phase: Review + * `2` - Submitting + * `3` - Submitted + + // Phase: Testing + * `4` - AcceptedUnvalidated // pending script review, can re-trigger validation + * `5` - Validating + * `6` - Validated + * `7` - Uploading + * `8` - Uploaded // uploaded to the group, but pending release + + // Phase: Final SubmissionStatus + * `9` - Rejected + * `10` - Released responses: "200": description: Successful response -- 2.49.1 From 0e1d2fe50a9d11955496069d75f3cd14e682041a Mon Sep 17 00:00:00 2001 From: Quaternions Date: Sat, 7 Jun 2025 17:09:51 -0700 Subject: [PATCH 06/11] openapi: generate --- pkg/api/oas_parameters_gen.go | 46 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index bcfd93d..262f791 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -2879,16 +2879,25 @@ func decodeListMapfixAuditEventsParams(args [1]string, argsEscaped bool, r *http // ListMapfixesParams is parameters of listMapfixes operation. type ListMapfixesParams struct { - Page int32 - Limit int32 - DisplayName OptString - Creator OptString - GameID OptInt32 + Page int32 + Limit int32 + DisplayName OptString + Creator OptString + // Game ID: * `1` - Bhop * `2` - Surf * `5` - FlyTrials. + GameID OptInt32 + // Sort order: * `0` - Disabled * `1` - DisplayNameAscending * `2` - DisplayNameDescending * `3` - + // DateAscending * `4` - DateDescending. Sort OptInt32 Submitter OptInt64 AssetID OptInt64 TargetAssetID OptInt64 - StatusID OptInt32 + // // Phase: Creation * `0` - UnderConstruction * `1` - ChangesRequested + // // Phase: Review * `2` - Submitting * `3` - Submitted + // // Phase: Testing * `4` - AcceptedUnvalidated // pending script review, can re-trigger validation + // * `5` - Validating * `6` - Validated * `7` - Uploading + // // Phase: Final MapfixStatus * `8` - Uploaded // uploaded to the group, but pending release * `9` + // - Rejected. + StatusID OptInt32 } func unpackListMapfixesParams(packed middleware.Parameters) (params ListMapfixesParams) { @@ -3617,7 +3626,9 @@ type ListMapsParams struct { DisplayName OptString Creator OptString GameID OptInt32 - Sort OptInt32 + // Sort order: * `0` - Disabled * `1` - DisplayNameAscending * `2` - DisplayNameDescending * `3` - + // DateAscending * `4` - DateDescending. + Sort OptInt32 } func unpackListMapsParams(packed middleware.Parameters) (params ListMapsParams) { @@ -5117,16 +5128,25 @@ func decodeListSubmissionAuditEventsParams(args [1]string, argsEscaped bool, r * // ListSubmissionsParams is parameters of listSubmissions operation. type ListSubmissionsParams struct { - Page int32 - Limit int32 - DisplayName OptString - Creator OptString - GameID OptInt32 + Page int32 + Limit int32 + DisplayName OptString + Creator OptString + // Game ID: * `1` - Bhop * `2` - Surf * `5` - FlyTrials. + GameID OptInt32 + // Sort order: * `0` - Disabled * `1` - DisplayNameAscending * `2` - DisplayNameDescending * `3` - + // DateAscending * `4` - DateDescending. Sort OptInt32 Submitter OptInt64 AssetID OptInt64 UploadedAssetID OptInt64 - StatusID OptInt32 + // // Phase: Creation * `0` - UnderConstruction * `1` - ChangesRequested + // // Phase: Review * `2` - Submitting * `3` - Submitted + // // Phase: Testing * `4` - AcceptedUnvalidated // pending script review, can re-trigger validation + // * `5` - Validating * `6` - Validated * `7` - Uploading * `8` - Uploaded // uploaded to the group, + // but pending release + // // Phase: Final SubmissionStatus * `9` - Rejected * `10` - Released. + StatusID OptInt32 } func unpackListSubmissionsParams(packed middleware.Parameters) (params ListSubmissionsParams) { -- 2.49.1 From 54bf3f55a02af9399e08a3e07b1ea6b064b63192 Mon Sep 17 00:00:00 2001 From: itzaname Date: Sun, 8 Jun 2025 03:41:36 +0000 Subject: [PATCH 07/11] Rework submission/mapfix/maps list views (#173) Refactored maps/landing/mapfix/submission and navbar Reviewed-on: https://git.itzana.me/StrafesNET/maps-service/pulls/173 Reviewed-by: Quaternions Co-authored-by: itzaname Co-committed-by: itzaname --- web/src/app/_components/carousel.tsx | 151 ++++++++++++ web/src/app/_components/header.tsx | 166 ++++++++++---- web/src/app/_components/mapCard.tsx | 315 ++++++++++++++++++++----- web/src/app/layout.tsx | 9 +- web/src/app/lib/theme.tsx | 91 ++++++++ web/src/app/mapfixes/page.tsx | 177 +++++++------- web/src/app/maps/page.tsx | 330 +++++++++++++++++++++++---- web/src/app/page.tsx | 223 +++++++++++++++++- web/src/app/submissions/page.tsx | 181 ++++++++------- web/src/app/ts/Mapfix.ts | 2 +- web/src/app/ts/Submission.ts | 2 +- 11 files changed, 1332 insertions(+), 315 deletions(-) create mode 100644 web/src/app/_components/carousel.tsx create mode 100644 web/src/app/lib/theme.tsx diff --git a/web/src/app/_components/carousel.tsx b/web/src/app/_components/carousel.tsx new file mode 100644 index 0000000..c8c26d2 --- /dev/null +++ b/web/src/app/_components/carousel.tsx @@ -0,0 +1,151 @@ +import {Box, IconButton, Typography} from "@mui/material"; +import {useEffect, useRef, useState} from "react"; +import Link from "next/link"; +import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import {SubmissionInfo} from "@/app/ts/Submission"; +import {MapfixInfo} from "@/app/ts/Mapfix"; + +// Type for the items in the carousel +type CarouselItem = SubmissionInfo | MapfixInfo; + +// Props for the Carousel component +interface CarouselProps { + title: string; + items: T[] | undefined; + renderItem: (item: T) => React.ReactNode; + viewAllLink: string; +} + +export function Carousel({ title, items, renderItem, viewAllLink }: CarouselProps) { + const carouselRef = useRef(null); + const [scrollPosition, setScrollPosition] = useState(0); + const [maxScroll, setMaxScroll] = useState(0); + + const SCROLL_AMOUNT = 300; + + useEffect(() => { + if (carouselRef.current) { + const scrollWidth = carouselRef.current.scrollWidth; + const clientWidth = carouselRef.current.clientWidth; + setMaxScroll(scrollWidth - clientWidth); + } + }, [items]); + + const scroll = (direction: 'left' | 'right'): void => { + if (carouselRef.current) { + const scrollAmount = direction === 'left' ? -SCROLL_AMOUNT : SCROLL_AMOUNT; + + carouselRef.current.scrollBy({ + left: scrollAmount, + behavior: 'smooth' + }); + + setTimeout(() => { + if (carouselRef.current) { + setScrollPosition(carouselRef.current.scrollLeft); + } + }, 300); + } + }; + + useEffect(() => { + const handleScroll = () => { + if (carouselRef.current) { + setScrollPosition(carouselRef.current.scrollLeft); + } + }; + + const ref = carouselRef.current; + if (ref) { + ref.addEventListener('scroll', handleScroll); + return () => ref.removeEventListener('scroll', handleScroll); + } + }, []); + + return ( + + + + {title} + + + + View All → + + + + + + scroll('left')} + > + + + + + {items?.map((item, index) => ( + + {renderItem(item)} + + ))} + + + = maxScroll - 5 ? 'hidden' : 'visible', + }} + onClick={() => scroll('right')} + > + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/header.tsx b/web/src/app/_components/header.tsx index b611533..463d62e 100644 --- a/web/src/app/_components/header.tsx +++ b/web/src/app/_components/header.tsx @@ -3,62 +3,132 @@ import Link from "next/link" import Image from "next/image"; -import "./styles/header.scss" -import { UserInfo } from "@/app/ts/User"; -import { useState, useEffect } from "react"; +import {UserInfo} from "@/app/ts/User"; +import {useState, useEffect} from "react"; + +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; interface HeaderButton { - name: string, - href: string + name: string; + href: string; } + function HeaderButton(header: HeaderButton) { - return ( - - - - ) + return ( + + ); } export default function Header() { - const handleLoginClick = () => { - window.location.href = "/auth/oauth2/login?redirect=" + window.location.href; - }; + const handleLoginClick = () => { + window.location.href = + "/auth/oauth2/login?redirect=" + window.location.href; + }; - const [valid, setValid] = useState(false) - const [user, setUser] = useState(null) + const [valid, setValid] = useState(false); + const [user, setUser] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); - useEffect(() => { - async function getLoginInfo() { - const [validateData, userData] = await Promise.all([ - fetch("/api/session/validate").then(validateResponse => validateResponse.json()), - fetch("/api/session/user").then(userResponse => userResponse.json()) - ]); - setValid(validateData) - setUser(userData) - } - getLoginInfo() - }, []) + const handleMenuOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; - return ( -
- - -
- ) + const handleMenuClose = () => { + setAnchorEl(null); + }; + + useEffect(() => { + async function getLoginInfo() { + try { + const response = await fetch("/api/session/user"); + + if (!response.ok) { + setValid(false); + setUser(null); + return; + } + + const userData = await response.json(); + const isLoggedIn = userData && 'UserID' in userData; + + setValid(isLoggedIn); + setUser(isLoggedIn ? userData : null); + } catch (error) { + console.error("Error fetching user data:", error); + setValid(false); + setUser(null); + } + } + + getLoginInfo(); + }, []); + + return ( + + + + + + + + + {valid && user && ( + + )} + {valid && user ? ( + + + + + Manage + + + + ) : ( + + )} + + + + ); } diff --git a/web/src/app/_components/mapCard.tsx b/web/src/app/_components/mapCard.tsx index 539c08e..ff46e5d 100644 --- a/web/src/app/_components/mapCard.tsx +++ b/web/src/app/_components/mapCard.tsx @@ -1,71 +1,268 @@ -import React from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { Rating } from "@mui/material"; +import React, {JSX} from "react"; +import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Chip, Divider, Grid, Typography} from "@mui/material"; +import {Cancel, CheckCircle, Explore, Pending, Person2} from "@mui/icons-material"; -interface SubmissionCardProps { +interface MapCardProps { displayName: string; assetId: number; authorId: number; author: string; rating: number; id: number; + statusID: number; + gameID: number; + created: number; + type: 'mapfix' | 'submission'; } -export function SubmissionCard(props: SubmissionCardProps) { - return ( - -
-
-
- {/* TODO: Grab image of model */} - {props.displayName} -
-
-
- {props.displayName} -
- -
-
-
-
- {props.author}/ - {props.author} -
-
-
-
-
- - ); -} +const CARD_WIDTH = 270; -export function MapfixCard(props: SubmissionCardProps) { +export function MapCard(props: MapCardProps) { + const StatusChip = ({status}: { status: number }) => { + let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default'; + let icon: JSX.Element = ; + let label: string = 'Unknown'; + + switch (status) { + case 0: + color = 'warning'; + icon = ; + label = 'Under Construction'; + break; + case 1: + color = 'warning'; + icon = ; + label = 'Changes Requested'; + break; + case 2: + color = 'info'; + icon = ; + label = 'Submitting'; + break; + case 3: + color = 'warning'; + icon = ; + label = 'Under Review'; + break; + case 4: + color = 'warning'; + icon = ; + label = 'Accepted Unvalidated'; + break; + case 5: + color = 'info'; + icon = ; + label = 'Validating'; + break; + case 6: + color = 'success'; + icon = ; + label = 'Validated'; + break; + case 7: + color = 'info'; + icon = ; + label = 'Uploading'; + break; + case 8: + color = 'success'; + icon = ; + label = 'Uploaded'; + break; + case 9: + color = 'error'; + icon = ; + label = 'Rejected'; + break; + case 10: + color = 'success'; + icon = ; + label = 'Released'; + break; + default: + color = 'default'; + icon = ; + label = 'Unknown'; + break; + } + + return ( + + ); + }; return ( - -
-
-
- {/* TODO: Grab image of model */} - {props.displayName} -
-
-
- {props.displayName} -
- -
-
-
-
- {props.author}/ - {props.author} -
-
-
-
-
- - ); + + + + + + + + + + + + + + {props.displayName} + + + + + {props.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 5 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'} + + + + + + {props.author} + + + + + + + + + {/*In the future author should be the username of the submitter not the info from the map*/} + {props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} + + + + + + + + + ) } diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index d524fd0..3fb5ba9 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,9 +1,16 @@ +'use client'; import "./globals.scss"; +import {theme} from "@/app/lib/theme"; +import {ThemeProvider} from "@mui/material"; export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) { return ( - {children} + + + {children} + + ); } \ No newline at end of file diff --git a/web/src/app/lib/theme.tsx b/web/src/app/lib/theme.tsx new file mode 100644 index 0000000..07d46c7 --- /dev/null +++ b/web/src/app/lib/theme.tsx @@ -0,0 +1,91 @@ +import {createTheme} from "@mui/material"; + +export const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#90caf9', + }, + secondary: { + main: '#f48fb1', + }, + background: { + default: '#121212', + paper: '#1e1e1e', + }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h5: { + fontWeight: 500, + letterSpacing: '0.5px', + }, + subtitle1: { + fontWeight: 500, + fontSize: '0.95rem', + }, + body2: { + fontSize: '0.875rem', + }, + caption: { + fontSize: '0.75rem', + }, + }, + shape: { + borderRadius: 8, + }, + components: { + MuiCard: { + styleOverrides: { + root: { + borderRadius: 8, + overflow: 'hidden', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out', + '&:hover': { + transform: 'translateY(-4px)', + boxShadow: '0 8px 16px rgba(0, 0, 0, 0.2)', + }, + }, + }, + }, + MuiCardMedia: { + styleOverrides: { + root: { + borderBottom: '1px solid rgba(255, 255, 255, 0.1)', + }, + }, + }, + MuiCardContent: { + styleOverrides: { + root: { + padding: 16, + '&:last-child': { + paddingBottom: 16, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + fontWeight: 500, + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + }, +}); \ No newline at end of file diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx index e4674b6..7eba041 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -2,113 +2,130 @@ import { useState, useEffect } from "react"; import { MapfixList } from "../ts/Mapfix"; -import { MapfixCard } from "../_components/mapCard"; +import {MapCard} from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; // TODO: MAKE MAPFIX & SUBMISSIONS USE THE SAME COMPONENTS :angry: (currently too lazy) import "./(styles)/page.scss"; import { ListSortConstants } from "../ts/Sort"; +import {Box, Breadcrumbs, CircularProgress, Container, Pagination, Typography} from "@mui/material"; +import Link from "next/link"; export default function MapfixInfoPage() { const [mapfixes, setMapfixes] = useState(null) const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); const cardsPerPage = 24; // built to fit on a 1920x1080 monitor useEffect(() => { - async function fetchMapfixes() { - const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`) + const controller = new AbortController(); + + async function fetchMapFixes() { + setIsLoading(true); + const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: controller.signal, + }); if (res.ok) { - setMapfixes(await res.json()) + setMapfixes(await res.json()); } + setIsLoading(false); } - setTimeout(() => { - fetchMapfixes() - }, 50); - }, [currentPage]) + fetchMapFixes(); - if (!mapfixes) { + return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + }, [currentPage]); + + if (isLoading || !mapfixes) { return -
- Loading... -
-
- } - - const totalPages = Math.ceil(mapfixes.Total / cardsPerPage); - - const currentCards = mapfixes.Mapfixes.slice( - (currentPage - 1) * cardsPerPage, - currentPage * cardsPerPage - ); - - const nextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const prevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - if (mapfixes.Total == 0) { - return -
- Mapfixes list is empty. -
-
- } - - return ( - // TODO: Add filter settings & searchbar & page selector -
-
- {Array.from({ length: totalPages }).map((_, index) => ( - setCurrentPage(index+1)} - > - ))} -
-
- - - Page {currentPage} of {totalPages} - - -
-
- {currentCards.map((mapfix) => ( - - ))} -
+ + + + Loading mapfixes... + +
+
; + } + + const totalPages = Math.ceil(mapfixes.Total / cardsPerPage); + const currentCards = mapfixes.Mapfixes; + + return ( + + +
+ + + Home + + Mapfixes + + + Map Fixes + + + Explore all submitted fixes for maps from the community. + +
+ {currentCards.map((submission) => ( + + ))} +
+ +
+ setCurrentPage(page)} + variant="outlined" + shape="rounded" + /> +
+
+
+
) } diff --git a/web/src/app/maps/page.tsx b/web/src/app/maps/page.tsx index 7e62df3..b93aa66 100644 --- a/web/src/app/maps/page.tsx +++ b/web/src/app/maps/page.tsx @@ -1,60 +1,298 @@ "use client"; +import {useState, useEffect} from "react"; import Image from "next/image"; -import { useState, useEffect } from "react"; +import {useRouter} from "next/navigation"; import Webpage from "@/app/_components/webpage"; - -import "./(styles)/page.scss"; +import { + Box, + Container, + Typography, + Grid, + Card, + CardContent, + CardMedia, + CardActionArea, + TextField, + InputAdornment, + Pagination, + CircularProgress, + FormControl, + InputLabel, + Select, + MenuItem, + SelectChangeEvent, Breadcrumbs +} from "@mui/material"; +import {Search as SearchIcon} from "@mui/icons-material"; +import Link from "next/link"; interface Map { - ID: number; - DisplayName: string; - Creator: string; - GameID: number; - Date: number; + ID: number; + DisplayName: string; + Creator: string; + GameID: number; + Date: number; } -// TODO: should rewrite this entire page, just wanted to get a simple page working. This was written by chatgippity - export default function MapsPage() { - const [maps, setMaps] = useState([]); + const router = useRouter(); + const [maps, setMaps] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [gameFilter, setGameFilter] = useState("0"); // 0 means "All Maps" + const mapsPerPage = 12; + const requestPageSize = 100; - useEffect(() => { - const fetchMaps = async () => { - const res = await fetch("/api/maps?Page=1&Limit=100"); - const data: Map[] = await res.json(); - setMaps(data); - }; + useEffect(() => { + const fetchMaps = async () => { + // Just send it and load all maps hoping for the best + try { + setLoading(true); + let allMaps: Map[] = []; + let page = 1; + let hasMore = true; - fetchMaps(); - }, []); + while (hasMore) { + const res = await fetch(`/api/maps?Page=${page}&Limit=${requestPageSize}`); + const data: Map[] = await res.json(); + allMaps = [...allMaps, ...data]; + hasMore = data.length === requestPageSize; + page++; + } - const customLoader = ({ src }: { src: string }) => { - return src; - }; + setMaps(allMaps); + } catch (error) { + console.error("Failed to fetch maps:", error); + } finally { + setLoading(false); + } + }; - return ( - -
- {maps.map((map) => ( - - ))} -
-
- ); + fetchMaps(); + }, []); + + const handleGameFilterChange = (event: SelectChangeEvent) => { + setGameFilter(event.target.value); + setCurrentPage(1); + }; + + // Filter maps based on search query and game filter + const filteredMaps = maps.filter(map => { + const matchesSearch = + map.DisplayName.toLowerCase().includes(searchQuery.toLowerCase()) || + map.Creator.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesGameFilter = + gameFilter === "0" || // "All Maps" + map.GameID === parseInt(gameFilter); + + return matchesSearch && matchesGameFilter; + }); + + // Calculate pagination + const totalPages = Math.ceil(filteredMaps.length / mapsPerPage); + const currentMaps = filteredMaps.slice( + (currentPage - 1) * mapsPerPage, + currentPage * mapsPerPage + ); + + const handlePageChange = (_event: React.ChangeEvent, page: number) => { + setCurrentPage(page); + window.scrollTo({top: 0, behavior: 'smooth'}); + }; + + const handleMapClick = (mapId: number) => { + router.push(`/maps/${mapId}`); + }; + + const formatDate = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getGameName = (gameId: number) => { + switch (gameId) { + case 1: + return "Bhop"; + case 2: + return "Surf"; + case 5: + return "Fly Trials"; + default: + return "Unknown"; + } + }; + + const getGameLabelStyles = (gameId: number) => { + switch (gameId) { + case 1: // Bhop + return { + bgcolor: "info.main", + color: "white", + }; + case 2: // Surf + return { + bgcolor: "success.main", + color: "white", + }; + case 5: // Fly Trials + return { + bgcolor: "warning.main", + color: "white", + }; + default: // Unknown + return { + bgcolor: "grey.500", + color: "white", + }; + } + }; + + return ( + + + + + + Home + + Maps + + + Map Collection + + + Browse all community-created maps or find your favorites + + { + setSearchQuery(e.target.value); + setCurrentPage(1); + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{mb: 4}} + /> + + {loading ? ( + + + + ) : ( + <> + + + Showing {filteredMaps.length} {filteredMaps.length === 1 ? 'map' : 'maps'} + + + + Filter by Game + + + + + + {currentMaps.map((map) => ( + + + handleMapClick(map.ID)}> + + + {getGameName(map.GameID)} + + {map.DisplayName} + + + + {map.DisplayName} + + + By {map.Creator} + + + Added {formatDate(map.Date)} + + + + + + ))} + + + {totalPages > 1 && ( + + + + )} + + )} + + + + ); } \ No newline at end of file diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index f6de7c1..0ac6930 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,7 +1,228 @@ +'use client' + +import { useState, useEffect } from "react"; +import {MapfixInfo, MapfixList} from "./ts/Mapfix"; +import { MapCard } from "./_components/mapCard"; import Webpage from "./_components/webpage"; +import { ListSortConstants } from "./ts/Sort"; +import { + Box, + Container, + CircularProgress, + Typography, + Paper, +} from "@mui/material"; +import Link from "next/link"; +import {SubmissionInfo, SubmissionList} from "@/app/ts/Submission"; +import {Carousel} from "@/app/_components/carousel"; export default function Home() { + const [mapfixes, setMapfixes] = useState(null); + const [submissions, setSubmissions] = useState(null); + const [isLoadingMapfixes, setIsLoadingMapfixes] = useState(false); + const [isLoadingSubmissions, setIsLoadingSubmissions] = useState(false); + const itemsPerSection: number = 8; // Show more items for the carousel + + useEffect(() => { + const mapfixController = new AbortController(); + const submissionsController = new AbortController(); + + async function fetchMapFixes(): Promise { + setIsLoadingMapfixes(true); + try { + const res = await fetch(`/api/mapfixes?Page=1&Limit=${itemsPerSection}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: mapfixController.signal, + }); + if (res.ok) { + const data: MapfixList = await res.json(); + setMapfixes(data); + } + } catch (error) { + console.error("Failed to fetch mapfixes:", error); + } finally { + setIsLoadingMapfixes(false); + } + } + + async function fetchSubmissions(): Promise { + setIsLoadingSubmissions(true); + try { + const res = await fetch(`/api/submissions?Page=1&Limit=${itemsPerSection}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: submissionsController.signal, + }); + if (res.ok) { + const data: SubmissionList = await res.json(); + setSubmissions(data); + } + } catch (error) { + console.error("Failed to fetch submissions:", error); + } finally { + setIsLoadingSubmissions(false); + } + } + + fetchMapFixes(); + fetchSubmissions(); + + return () => { + mapfixController.abort(); + submissionsController.abort(); + }; + }, []); + + const isLoading: boolean = isLoadingMapfixes || isLoadingSubmissions; + + if (isLoading && (!mapfixes || !submissions)) { + return +
+ + + + Loading content... + + +
+
; + } + + const renderMapfixCard = (mapfix: MapfixInfo): React.ReactNode => ( + + ); + + const renderSubmissionCard = (submission: SubmissionInfo): React.ReactNode => ( + + ); + return ( - + + +
+ + Welcome to the Maps Service! + + + + Contribute to the community + + + Help improve maps by submitting fixes or creating new maps submissions for the community. + + + + + Submit Map + + + + + Create Map Fix + + + + + + {/* Submissions Carousel */} + {submissions && ( + + title="Recent Submissions" + items={submissions.Submissions} + renderItem={renderSubmissionCard} + viewAllLink="/submissions" + /> + )} + + {/* Map Fixes Carousel */} + {mapfixes && ( + + title="Recent Map Fixes" + items={mapfixes.Mapfixes} + renderItem={renderMapfixCard} + viewAllLink="/mapfixes" + /> + )} +
+
+
); } \ No newline at end of file diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index bd67b33..c81e090 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -1,112 +1,137 @@ 'use client' -import { useState, useEffect } from "react"; -import { SubmissionList } from "../ts/Submission"; -import { SubmissionCard } from "../_components/mapCard"; +import {useState, useEffect} from "react"; +import {SubmissionList} from "../ts/Submission"; +import {MapCard} from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; import "./(styles)/page.scss"; -import { ListSortConstants } from "../ts/Sort"; +import {ListSortConstants} from "../ts/Sort"; +import {Breadcrumbs, Pagination, Typography, CircularProgress, Box, Container} from "@mui/material"; +import Link from "next/link"; export default function SubmissionInfoPage() { - const [submissions, setSubmissions] = useState(null) + const [submissions, setSubmissions] = useState(null); const [currentPage, setCurrentPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); const cardsPerPage = 24; // built to fit on a 1920x1080 monitor useEffect(() => { + const controller = new AbortController(); + async function fetchSubmissions() { - const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`) + setIsLoading(true); + const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { + signal: controller.signal, + }); if (res.ok) { - setSubmissions(await res.json()) + setSubmissions(await res.json()); } + setIsLoading(false); } - setTimeout(() => { - fetchSubmissions() - }, 50); - }, [currentPage]) + fetchSubmissions(); - if (!submissions) { + return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + }, [currentPage]); + + if (isLoading || !submissions) { return -
- Loading... +
+ + + + Loading submissions... + +
- + ; } const totalPages = Math.ceil(submissions.Total / cardsPerPage); + const currentCards = submissions.Submissions; - const currentCards = submissions.Submissions.slice( - (currentPage - 1) * cardsPerPage, - currentPage * cardsPerPage - ); - - const nextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const prevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - if (submissions.Total == 0) { + if (submissions.Total === 0) { return
Submissions list is empty.
-
+ ; } return ( - // TODO: Add filter settings & searchbar & page selector -
-
- {Array.from({ length: totalPages }).map((_, index) => ( - setCurrentPage(index+1)} - > - ))} -
-
- - - Page {currentPage} of {totalPages} - - -
-
- {currentCards.map((submission) => ( - - ))} -
-
+ +
+ + + Home + + Submissions + + + Submissions + + + Explore all submitted maps from the community. + +
+ {currentCards.map((submission) => ( + + ))} +
+ +
+ setCurrentPage(page)} + variant="outlined" + shape="rounded" + /> +
+
+
+
) } diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts index c4a9bce..a09ed2b 100644 --- a/web/src/app/ts/Mapfix.ts +++ b/web/src/app/ts/Mapfix.ts @@ -17,7 +17,7 @@ interface MapfixInfo { readonly DisplayName: string, readonly Creator: string, readonly GameID: number, - readonly Date: number, + readonly CreatedAt: number, readonly Submitter: number, readonly AssetID: number, readonly AssetVersion: number, diff --git a/web/src/app/ts/Submission.ts b/web/src/app/ts/Submission.ts index 4904726..89d9cd8 100644 --- a/web/src/app/ts/Submission.ts +++ b/web/src/app/ts/Submission.ts @@ -17,7 +17,7 @@ interface SubmissionInfo { readonly DisplayName: string, readonly Creator: string, readonly GameID: number, - readonly Date: number, + readonly CreatedAt: number, readonly Submitter: number, readonly AssetID: number, readonly AssetVersion: number, -- 2.49.1 From ec59a833799a8fc391edcc1ab8360bb82d4410ae Mon Sep 17 00:00:00 2001 From: Quaternions Date: Sat, 7 Jun 2025 22:33:52 -0700 Subject: [PATCH 08/11] validation: include more details in duplicates error --- validation/api/src/external.rs | 8 ++++---- validation/api/src/internal.rs | 8 ++++---- validation/api/src/types.rs | 13 +++++++++---- validation/src/validator.rs | 4 ++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/validation/api/src/external.rs b/validation/api/src/external.rs index e5b9cb0..4d19bef 100644 --- a/validation/api/src/external.rs +++ b/validation/api/src/external.rs @@ -46,7 +46,7 @@ impl Context{ ).await.map_err(Error::Response)? .json().await.map_err(Error::ReqwestJson) } - pub async fn get_script_from_hash(&self,config:HashRequest<'_>)->Result,SingleItemError>{ + pub async fn get_script_from_hash(&self,config:HashRequest<'_>)->Result,ScriptSingleItemError>{ let scripts=self.get_scripts(GetScriptsRequest{ Page:1, Limit:2, @@ -57,7 +57,7 @@ impl Context{ ResourceID:None, }).await.map_err(SingleItemError::Other)?; if 1)->Result,SingleItemError>{ + pub async fn get_script_policy_from_hash(&self,config:HashRequest<'_>)->Result,ScriptPolicySingleItemError>{ let policies=self.get_script_policies(GetScriptPoliciesRequest{ Page:1, Limit:2, @@ -115,7 +115,7 @@ impl Context{ Policy:None, }).await.map_err(SingleItemError::Other)?; if 1)->Result,SingleItemError>{ + pub async fn get_script_from_hash(&self,config:HashRequest<'_>)->Result,ScriptSingleItemError>{ let scripts=self.get_scripts(GetScriptsRequest{ Page:1, Limit:2, @@ -87,7 +87,7 @@ impl Context{ ResourceID:None, }).await.map_err(SingleItemError::Other)?; if 1)->Result,SingleItemError>{ + pub async fn get_script_policy_from_hash(&self,config:HashRequest<'_>)->Result,ScriptPolicySingleItemError>{ let policies=self.get_script_policies(GetScriptPoliciesRequest{ Page:1, Limit:2, @@ -135,7 +135,7 @@ impl Context{ Policy:None, }).await.map_err(SingleItemError::Other)?; if 1{ + DuplicateItems(Items), Other(Error), } -impl std::fmt::Display for SingleItemError{ +impl std::fmt::Display for SingleItemError +where + Items:std::fmt::Debug +{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"{self:?}") } } -impl std::error::Error for SingleItemError{} +impl std::error::Error for SingleItemError where Items:std::fmt::Debug{} +pub type ScriptSingleItemError=SingleItemError>; +pub type ScriptPolicySingleItemError=SingleItemError>; #[allow(dead_code)] #[derive(Debug)] diff --git a/validation/src/validator.rs b/validation/src/validator.rs index 26ac08c..e5ef087 100644 --- a/validation/src/validator.rs +++ b/validation/src/validator.rs @@ -39,11 +39,11 @@ pub enum Error{ ScriptNotYetReviewed(Option), Download(crate::download::Error), ModelFileDecode(ReadDomError), - ApiGetScriptPolicyFromHash(submissions_api::types::SingleItemError), + ApiGetScriptPolicyFromHash(submissions_api::types::ScriptPolicySingleItemError), ApiGetScript(submissions_api::Error), ApiCreateScript(submissions_api::Error), ApiCreateScriptPolicy(submissions_api::Error), - ApiGetScriptFromHash(submissions_api::types::SingleItemError), + ApiGetScriptFromHash(submissions_api::types::ScriptSingleItemError), ApiUpdateMapfixModel(submissions_api::Error), ApiUpdateSubmissionModel(submissions_api::Error), ModelFileRootMustHaveOneChild, -- 2.49.1 From 4d78a9b2c517d78b9f672c6f405ec9ec90ac5224 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Sat, 7 Jun 2025 22:42:39 -0700 Subject: [PATCH 09/11] submissions: add missing audit even when requesting changes for a mapfix --- pkg/service/mapfixes.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pkg/service/mapfixes.go b/pkg/service/mapfixes.go index 4e69b52..c43f708 100644 --- a/pkg/service/mapfixes.go +++ b/pkg/service/mapfixes.go @@ -396,10 +396,42 @@ func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.A return ErrPermissionDeniedNeedRoleMapfixReview } + userId, err := userInfo.GetUserID() + if err != nil { + return err + } + // transaction + target_status := model.MapfixStatusChangesRequested smap := datastore.Optional() - smap.Add("status_id", model.MapfixStatusChangesRequested) - return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}, smap) + smap.Add("status_id", target_status) + err = svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}, smap) + if err != nil { + return err + } + + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + EventData, err := json.Marshal(event_data) + if err != nil { + return err + } + + _, err = svc.DB.AuditEvents().Create(ctx, model.AuditEvent{ + ID: 0, + User: userId, + ResourceType: model.ResourceMapfix, + ResourceID: params.MapfixID, + EventType: model.AuditEventTypeAction, + EventData: EventData, + }) + if err != nil { + return err + } + + return nil } // ActionMapfixRevoke invokes actionMapfixRevoke operation. -- 2.49.1 From 8f2a0b53e495c505abb98553bd78cb3af19d2289 Mon Sep 17 00:00:00 2001 From: itzaname Date: Mon, 9 Jun 2025 00:33:27 +0000 Subject: [PATCH 10/11] Refactor remaining frontend pages (#183) Reviewed-on: https://git.itzana.me/StrafesNET/maps-service/pulls/183 Reviewed-by: Quaternions Co-authored-by: itzaname Co-committed-by: itzaname --- web/package.json | 5 +- web/src/app/_components/ErrorDisplay.tsx | 43 ++ .../_components/comments/AuditEventItem.tsx | 52 ++ .../comments/AuditEventsTabPanel.tsx | 39 ++ .../app/_components/comments/CommentItem.tsx | 52 ++ .../comments/CommentsAndAuditSection.tsx | 65 +++ .../_components/comments/CommentsTabPanel.tsx | 92 ++++ web/src/app/_components/header.tsx | 139 ++++- web/src/app/_components/mapCard.tsx | 90 +-- .../app/_components/review/CopyableField.tsx | 38 ++ .../app/_components/review/ReviewButtons.tsx | 174 ++++++ web/src/app/_components/review/ReviewItem.tsx | 86 +++ .../_components/review/ReviewItemHeader.tsx | 36 ++ web/src/app/_components/statusChip.tsx | 87 +++ web/src/app/mapfixes/(styles)/page.scss | 75 --- .../mapfixes/[mapfixId]/(styles)/page.scss | 19 - .../(styles)/page/commentWindow.scss | 56 -- .../[mapfixId]/(styles)/page/comments.scss | 49 -- .../[mapfixId]/(styles)/page/map.scss | 15 - .../(styles)/page/ratingWindow.scss | 43 -- .../[mapfixId]/(styles)/page/review.scss | 47 -- .../(styles)/page/reviewButtons.scss | 13 - .../(styles)/page/reviewStatus.scss | 80 --- web/src/app/mapfixes/[mapfixId]/_comments.tsx | 71 --- web/src/app/mapfixes/[mapfixId]/_mapImage.tsx | 31 -- .../mapfixes/[mapfixId]/_reviewButtons.tsx | 169 ------ web/src/app/mapfixes/[mapfixId]/_window.tsx | 20 - web/src/app/mapfixes/[mapfixId]/page.tsx | 511 ++++++++++++++---- web/src/app/mapfixes/page.tsx | 151 +++--- web/src/app/maps/(styles)/page.scss | 43 -- web/src/app/maps/[mapId]/_mapImage.tsx | 28 - .../app/maps/[mapId]/fix/(styles)/page.scss | 54 -- web/src/app/maps/[mapId]/fix/page.tsx | 278 ++++++++-- web/src/app/maps/[mapId]/page.tsx | 391 +++++++++++--- web/src/app/maps/page.tsx | 14 +- .../[operationId]/(styles)/page.scss | 91 ---- web/src/app/operations/[operationId]/page.tsx | 208 +++++-- web/src/app/submissions/(styles)/page.scss | 75 --- .../[submissionId]/(styles)/page.scss | 19 - .../(styles)/page/commentWindow.scss | 56 -- .../(styles)/page/comments.scss | 49 -- .../[submissionId]/(styles)/page/map.scss | 19 - .../(styles)/page/ratingWindow.scss | 43 -- .../[submissionId]/(styles)/page/review.scss | 46 -- .../(styles)/page/reviewButtons.scss | 13 - .../(styles)/page/reviewStatus.scss | 80 --- .../submissions/[submissionId]/_comments.tsx | 70 --- .../submissions/[submissionId]/_mapImage.tsx | 28 - .../[submissionId]/_reviewButtons.tsx | 172 ------ .../submissions/[submissionId]/_window.tsx | 20 - .../app/submissions/[submissionId]/page.tsx | 389 +++++++++---- web/src/app/submissions/page.tsx | 147 ++--- web/src/app/submit/(styles)/page.scss | 54 -- web/src/app/submit/page.tsx | 184 +++++-- web/src/app/ts/AuditEvent.ts | 2 +- web/src/app/ts/Submission.ts | 4 +- 56 files changed, 2621 insertions(+), 2304 deletions(-) create mode 100644 web/src/app/_components/ErrorDisplay.tsx create mode 100644 web/src/app/_components/comments/AuditEventItem.tsx create mode 100644 web/src/app/_components/comments/AuditEventsTabPanel.tsx create mode 100644 web/src/app/_components/comments/CommentItem.tsx create mode 100644 web/src/app/_components/comments/CommentsAndAuditSection.tsx create mode 100644 web/src/app/_components/comments/CommentsTabPanel.tsx create mode 100644 web/src/app/_components/review/CopyableField.tsx create mode 100644 web/src/app/_components/review/ReviewButtons.tsx create mode 100644 web/src/app/_components/review/ReviewItem.tsx create mode 100644 web/src/app/_components/review/ReviewItemHeader.tsx create mode 100644 web/src/app/_components/statusChip.tsx delete mode 100644 web/src/app/mapfixes/(styles)/page.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss delete mode 100644 web/src/app/mapfixes/[mapfixId]/_comments.tsx delete mode 100644 web/src/app/mapfixes/[mapfixId]/_mapImage.tsx delete mode 100644 web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx delete mode 100644 web/src/app/mapfixes/[mapfixId]/_window.tsx delete mode 100644 web/src/app/maps/(styles)/page.scss delete mode 100644 web/src/app/maps/[mapId]/_mapImage.tsx delete mode 100644 web/src/app/maps/[mapId]/fix/(styles)/page.scss delete mode 100644 web/src/app/operations/[operationId]/(styles)/page.scss delete mode 100644 web/src/app/submissions/(styles)/page.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/commentWindow.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/comments.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/map.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/ratingWindow.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/review.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/reviewButtons.scss delete mode 100644 web/src/app/submissions/[submissionId]/(styles)/page/reviewStatus.scss delete mode 100644 web/src/app/submissions/[submissionId]/_comments.tsx delete mode 100644 web/src/app/submissions/[submissionId]/_mapImage.tsx delete mode 100644 web/src/app/submissions/[submissionId]/_reviewButtons.tsx delete mode 100644 web/src/app/submissions/[submissionId]/_window.tsx delete mode 100644 web/src/app/submit/(styles)/page.scss diff --git a/web/package.json b/web/package.json index 5342771..3e91a77 100644 --- a/web/package.json +++ b/web/package.json @@ -13,18 +13,19 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^6.1.10", "@mui/material": "^6.1.10", + "date-fns": "^4.1.0", "next": "^15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "sass": "^1.82.0" }, "devDependencies": { - "typescript": "^5.7.2", + "@eslint/eslintrc": "^3.2.0", "@types/node": "^20.17.9", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "eslint": "^9.16.0", "eslint-config-next": "15.1.0", - "@eslint/eslintrc": "^3.2.0" + "typescript": "^5.7.2" } } diff --git a/web/src/app/_components/ErrorDisplay.tsx b/web/src/app/_components/ErrorDisplay.tsx new file mode 100644 index 0000000..af7ea0e --- /dev/null +++ b/web/src/app/_components/ErrorDisplay.tsx @@ -0,0 +1,43 @@ +import { Button, Container, Paper, Typography } from "@mui/material"; +import Webpage from "@/app/_components/webpage"; + +interface ErrorDisplayProps { + title: string; + message: string; + buttonText?: string; + onButtonClick?: () => void; +} + +export function ErrorDisplay({ + title, + message, + buttonText, + onButtonClick + }: ErrorDisplayProps) { + return ( + + + + {title} + {message} + {buttonText && onButtonClick && ( + + )} + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/comments/AuditEventItem.tsx b/web/src/app/_components/comments/AuditEventItem.tsx new file mode 100644 index 0000000..f4a2989 --- /dev/null +++ b/web/src/app/_components/comments/AuditEventItem.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + Box, + Avatar, + Typography, + Tooltip +} from "@mui/material"; +import PersonIcon from '@mui/icons-material/Person'; +import { formatDistanceToNow, format } from "date-fns"; +import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent"; + +interface AuditEventItemProps { + event: AuditEvent; + validatorUser: number; +} + +export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) { + return ( + + + + + + + + {event.User === validatorUser ? "Validator" : event.Username || "Unknown"} + + + + {auditEventMessage(event)} + + + ); +} + +interface DateDisplayProps { + date: number; +} + +function DateDisplay({ date }: DateDisplayProps) { + return ( + + + + {formatDistanceToNow(new Date(date * 1000), { addSuffix: true })} + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/comments/AuditEventsTabPanel.tsx b/web/src/app/_components/comments/AuditEventsTabPanel.tsx new file mode 100644 index 0000000..d5b68cc --- /dev/null +++ b/web/src/app/_components/comments/AuditEventsTabPanel.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { + Box, + Stack, +} from "@mui/material"; +import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent"; +import AuditEventItem from './AuditEventItem'; + +interface AuditEventsTabPanelProps { + activeTab: number; + auditEvents: AuditEvent[]; + validatorUser: number; +} + +export default function AuditEventsTabPanel({ + activeTab, + auditEvents, + validatorUser + }: AuditEventsTabPanelProps) { + const filteredEvents = auditEvents.filter( + event => event.EventType !== AuditEventType.Comment + ); + + return ( + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/comments/CommentItem.tsx b/web/src/app/_components/comments/CommentItem.tsx new file mode 100644 index 0000000..a385244 --- /dev/null +++ b/web/src/app/_components/comments/CommentItem.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + Box, + Avatar, + Typography, + Tooltip +} from "@mui/material"; +import PersonIcon from '@mui/icons-material/Person'; +import { formatDistanceToNow, format } from "date-fns"; +import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent"; + +interface CommentItemProps { + event: AuditEvent; + validatorUser: number; +} + +export default function CommentItem({ event, validatorUser }: CommentItemProps) { + return ( + + + + + + + + {event.User === validatorUser ? "Validator" : event.Username || "Unknown"} + + + + {decodeAuditEvent(event)} + + + ); +} + +interface DateDisplayProps { + date: number; +} + +function DateDisplay({ date }: DateDisplayProps) { + return ( + + + + {formatDistanceToNow(new Date(date * 1000), { addSuffix: true })} + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/comments/CommentsAndAuditSection.tsx b/web/src/app/_components/comments/CommentsAndAuditSection.tsx new file mode 100644 index 0000000..274304b --- /dev/null +++ b/web/src/app/_components/comments/CommentsAndAuditSection.tsx @@ -0,0 +1,65 @@ +import React, {useState} from 'react'; +import { + Paper, + Box, + Tabs, + Tab, +} from "@mui/material"; +import CommentsTabPanel from './CommentsTabPanel'; +import AuditEventsTabPanel from './AuditEventsTabPanel'; +import { AuditEvent } from "@/app/ts/AuditEvent"; + +interface CommentsAndAuditSectionProps { + auditEvents: AuditEvent[]; + newComment: string; + setNewComment: (comment: string) => void; + handleCommentSubmit: () => void; + validatorUser: number; + userId: number | null; +} + +export default function CommentsAndAuditSection({ + auditEvents, + newComment, + setNewComment, + handleCommentSubmit, + validatorUser, + userId, + }: CommentsAndAuditSectionProps) { + + const [activeTab, setActiveTab] = useState(0); + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; + + return ( + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/comments/CommentsTabPanel.tsx b/web/src/app/_components/comments/CommentsTabPanel.tsx new file mode 100644 index 0000000..7318b35 --- /dev/null +++ b/web/src/app/_components/comments/CommentsTabPanel.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { + Box, + Stack, + Avatar, + TextField, + IconButton +} from "@mui/material"; +import SendIcon from '@mui/icons-material/Send'; +import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent"; +import CommentItem from './CommentItem'; + +interface CommentsTabPanelProps { + activeTab: number; + auditEvents: AuditEvent[]; + validatorUser: number; + newComment: string; + setNewComment: (comment: string) => void; + handleCommentSubmit: () => void; + userId: number | null; +} + +export default function CommentsTabPanel({ + activeTab, + auditEvents, + validatorUser, + newComment, + setNewComment, + handleCommentSubmit, + userId + }: CommentsTabPanelProps) { + const commentEvents = auditEvents.filter( + event => event.EventType === AuditEventType.Comment + ); + + return ( + + ); +} + +interface CommentInputProps { + newComment: string; + setNewComment: (comment: string) => void; + handleCommentSubmit: () => void; + userId: number | null; +} + +function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) { + return ( + + + setNewComment(e.target.value)} + /> + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/_components/header.tsx b/web/src/app/_components/header.tsx index 463d62e..0eac6b4 100644 --- a/web/src/app/_components/header.tsx +++ b/web/src/app/_components/header.tsx @@ -2,9 +2,8 @@ import Link from "next/link" import Image from "next/image"; - -import {UserInfo} from "@/app/ts/User"; -import {useState, useEffect} from "react"; +import { UserInfo } from "@/app/ts/User"; +import { useState, useEffect } from "react"; import AppBar from "@mui/material/AppBar"; import Toolbar from "@mui/material/Toolbar"; @@ -13,12 +12,27 @@ import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import IconButton from "@mui/material/IconButton"; +import MenuIcon from "@mui/icons-material/Menu"; +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; interface HeaderButton { name: string; href: string; } +const navItems: HeaderButton[] = [ + { name: "Submissions", href: "/submissions" }, + { name: "Mapfixes", href: "/mapfixes" }, + { name: "Maps", href: "/maps" }, +]; + function HeaderButton(header: HeaderButton) { return ( )} - {valid && user ? ( + {!isMobile && valid && user ? ( @@ -122,13 +200,46 @@ export default function Header() { - ) : ( + ) : !isMobile && ( )} + + {/* In mobile view, display just the avatar if logged in */} + {isMobile && valid && user && ( + + {user.Username} + + )} + + {/* Mobile drawer */} + + {drawer} + ); -} +} \ No newline at end of file diff --git a/web/src/app/_components/mapCard.tsx b/web/src/app/_components/mapCard.tsx index ff46e5d..c54030a 100644 --- a/web/src/app/_components/mapCard.tsx +++ b/web/src/app/_components/mapCard.tsx @@ -1,6 +1,7 @@ -import React, {JSX} from "react"; -import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Chip, Divider, Grid, Typography} from "@mui/material"; -import {Cancel, CheckCircle, Explore, Pending, Person2} from "@mui/icons-material"; +import React from "react"; +import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Grid, Typography} from "@mui/material"; +import {Explore, Person2} from "@mui/icons-material"; +import {StatusChip} from "@/app/_components/statusChip"; interface MapCardProps { displayName: string; @@ -18,89 +19,6 @@ interface MapCardProps { const CARD_WIDTH = 270; export function MapCard(props: MapCardProps) { - const StatusChip = ({status}: { status: number }) => { - let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default'; - let icon: JSX.Element = ; - let label: string = 'Unknown'; - - switch (status) { - case 0: - color = 'warning'; - icon = ; - label = 'Under Construction'; - break; - case 1: - color = 'warning'; - icon = ; - label = 'Changes Requested'; - break; - case 2: - color = 'info'; - icon = ; - label = 'Submitting'; - break; - case 3: - color = 'warning'; - icon = ; - label = 'Under Review'; - break; - case 4: - color = 'warning'; - icon = ; - label = 'Accepted Unvalidated'; - break; - case 5: - color = 'info'; - icon = ; - label = 'Validating'; - break; - case 6: - color = 'success'; - icon = ; - label = 'Validated'; - break; - case 7: - color = 'info'; - icon = ; - label = 'Uploading'; - break; - case 8: - color = 'success'; - icon = ; - label = 'Uploaded'; - break; - case 9: - color = 'error'; - icon = ; - label = 'Rejected'; - break; - case 10: - color = 'success'; - icon = ; - label = 'Released'; - break; - default: - color = 'default'; - icon = ; - label = 'Unknown'; - break; - } - - return ( - - ); - }; return ( void; + placeholderText?: string; +} + +export const CopyableField = ({ + label, + value, + onCopy, + placeholderText = "Not assigned" + }: CopyableFieldProps) => { + const displayValue = value?.toString() || placeholderText; + + return ( + <> + {label} + + {displayValue} + {value && ( + + onCopy(value.toString())} + sx={{ ml: 1 }} + > + + + + )} + + + ); +}; \ No newline at end of file diff --git a/web/src/app/_components/review/ReviewButtons.tsx b/web/src/app/_components/review/ReviewButtons.tsx new file mode 100644 index 0000000..9f59138 --- /dev/null +++ b/web/src/app/_components/review/ReviewButtons.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { Button, Stack } from '@mui/material'; +import {MapfixInfo } from "@/app/ts/Mapfix"; +import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles"; +import {SubmissionInfo, SubmissionStatus} from "@/app/ts/Submission"; + +interface ReviewAction { + name: string, + action: string, +} + +interface ReviewButtonsProps { + onClick: (action: string, id: number) => void; + item: (SubmissionInfo | MapfixInfo); + userId: number | null; + roles: Roles; + type: "submission" | "mapfix"; +} + +const ReviewActions = { + Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, + BypassSubmit: {name:"Bypass Submit", action:"bypass-submit"} as ReviewAction, + ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction, + Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, + Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, + Reject: {name:"Reject",action:"reject"} as ReviewAction, + Validate: {name:"Validate",action:"retry-validate"} as ReviewAction, + ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction, + RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, + Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, + ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction, +} + +const ReviewButtons: React.FC = ({ + onClick, + item, + userId, + roles, + type, + }) => { + const getVisibleButtons = () => { + if (!item || userId === null) return []; + + // Define a type for the button + type ReviewButton = { + action: ReviewAction; + color: "primary" | "error" | "success" | "info" | "warning"; + }; + + const buttons: ReviewButton[] = []; + const is_submitter = userId === item.Submitter; + const status = item.StatusID; + + // Helper function to check status regardless of which enum type it is + const statusMatches = (statusValues: number[]) => { + return statusValues.includes(status); + }; + + // Create status constants that work with both types + const Status = { + UnderConstruction: SubmissionStatus.UnderConstruction, + ChangesRequested: SubmissionStatus.ChangesRequested, + Submitting: SubmissionStatus.Submitting, + Submitted: SubmissionStatus.Submitted, + AcceptedUnvalidated: SubmissionStatus.AcceptedUnvalidated, + Validating: SubmissionStatus.Validating, + Validated: SubmissionStatus.Validated, + Uploading: SubmissionStatus.Uploading, + Uploaded: SubmissionStatus.Uploaded, + Rejected: SubmissionStatus.Rejected, + Release: SubmissionStatus.Released + }; + + const reviewRole = type === "submission" ? RolesConstants.SubmissionReview : RolesConstants.MapfixReview; + const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload; + + if (is_submitter) { + if (statusMatches([Status.UnderConstruction, Status.ChangesRequested])) { + buttons.push({ + action: ReviewActions.Submit, + color: "primary" + }); + } + + if (statusMatches([Status.Submitted, Status.ChangesRequested])) { + buttons.push({ + action: ReviewActions.Revoke, + color: "error" + }); + } + } + + // Buttons for review role + if (hasRole(roles, reviewRole)) { + if (status === Status.Submitted && !is_submitter) { + buttons.push( + { + action: ReviewActions.Accept, + color: "success" + }, + { + action: ReviewActions.Reject, + color: "error" + } + ); + } + + if (status === Status.AcceptedUnvalidated) { + buttons.push({ + action: ReviewActions.Validate, + color: "info" + }); + } + + if (status === Status.Validating) { + buttons.push({ + action: ReviewActions.ResetValidating, + color: "warning" + }); + } + + if (statusMatches([Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) { + buttons.push({ + action: ReviewActions.RequestChanges, + color: "warning" + }); + } + + if (status === Status.ChangesRequested) { + buttons.push({ + action: ReviewActions.BypassSubmit, + color: "warning" + }); + } + } + + // Buttons for upload role + if (hasRole(roles, uploadRole)) { + if (status === Status.Validated) { + buttons.push({ + action: ReviewActions.Upload, + color: "success" + }); + } + + if (status === Status.Uploading) { + buttons.push({ + action: ReviewActions.ResetUploading, + color: "warning" + }); + } + } + + return buttons; + }; + + return ( + + {getVisibleButtons().map((button, index) => ( + + ))} + + ); +}; + +export default ReviewButtons; \ No newline at end of file diff --git a/web/src/app/_components/review/ReviewItem.tsx b/web/src/app/_components/review/ReviewItem.tsx new file mode 100644 index 0000000..36fac18 --- /dev/null +++ b/web/src/app/_components/review/ReviewItem.tsx @@ -0,0 +1,86 @@ +import { Paper, Grid, Typography } from "@mui/material"; +import { ReviewItemHeader } from "./ReviewItemHeader"; +import { CopyableField } from "@/app/_components/review/CopyableField"; +import { SubmissionInfo } from "@/app/ts/Submission"; +import { MapfixInfo } from "@/app/ts/Mapfix"; + +// Define a field configuration for specific types +interface FieldConfig { + key: string; + label: string; + placeholder?: string; +} + +type ReviewItemType = SubmissionInfo | MapfixInfo; + +interface ReviewItemProps { + item: ReviewItemType; + handleCopyValue: (value: string) => void; +} + +export function ReviewItem({ + item, + handleCopyValue + }: ReviewItemProps) { + // Type guard to check if item is valid + if (!item) return null; + + // Determine the type of item + const isSubmission = 'UploadedAssetID' in item; + const isMapfix = 'TargetAssetID' in item; + + // Define static fields based on item type + let fields: FieldConfig[] = []; + if (isSubmission) { + // Fields for Submission + fields = [ + { key: 'Submitter', label: 'Submitter ID' }, + { key: 'AssetID', label: 'Asset ID' }, + { key: 'UploadedAssetID', label: 'Uploaded Asset ID' }, + ]; + } else if (isMapfix) { + // Fields for Mapfix + fields = [ + { key: 'Submitter', label: 'Submitter ID' }, + { key: 'AssetID', label: 'Asset ID' }, + { key: 'TargetAssetID', label: 'Target Asset ID' }, + ]; + } + + return ( + + + + {/* Item Details */} + + {fields.map((field) => ( + + + + ))} + + + {/* Description Section */} + {isMapfix && item.Description && ( +
+ + Description + + + {item.Description} + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/web/src/app/_components/review/ReviewItemHeader.tsx b/web/src/app/_components/review/ReviewItemHeader.tsx new file mode 100644 index 0000000..50ce78f --- /dev/null +++ b/web/src/app/_components/review/ReviewItemHeader.tsx @@ -0,0 +1,36 @@ +import {Typography, Box, Avatar} from "@mui/material"; +import { StatusChip } from "@/app/_components/statusChip"; +import { SubmissionStatus } from "@/app/ts/Submission"; +import { MapfixStatus } from "@/app/ts/Mapfix"; + +type StatusIdType = SubmissionStatus | MapfixStatus; + +interface ReviewItemHeaderProps { + displayName: string; + statusId: StatusIdType; + creator: string | null | undefined; + submitterId: number; +} + +export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId }: ReviewItemHeaderProps) => { + return ( + <> + + + {displayName} + + + + + + + + by {creator || "Unknown Creator"} + + + + ); +}; \ No newline at end of file diff --git a/web/src/app/_components/statusChip.tsx b/web/src/app/_components/statusChip.tsx new file mode 100644 index 0000000..05e36ff --- /dev/null +++ b/web/src/app/_components/statusChip.tsx @@ -0,0 +1,87 @@ +import React, {JSX} from "react"; +import {Cancel, CheckCircle, Pending} from "@mui/icons-material"; +import {Chip} from "@mui/material"; + +export const StatusChip = ({status}: { status: number }) => { + let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default'; + let icon: JSX.Element = ; + let label: string = 'Unknown'; + + switch (status) { + case 0: + color = 'warning'; + icon = ; + label = 'Under Construction'; + break; + case 1: + color = 'warning'; + icon = ; + label = 'Changes Requested'; + break; + case 2: + color = 'info'; + icon = ; + label = 'Submitting'; + break; + case 3: + color = 'warning'; + icon = ; + label = 'Under Review'; + break; + case 4: + color = 'warning'; + icon = ; + label = 'Script Review'; + break; + case 5: + color = 'info'; + icon = ; + label = 'Validating'; + break; + case 6: + color = 'success'; + icon = ; + label = 'Validated'; + break; + case 7: + color = 'info'; + icon = ; + label = 'Uploading'; + break; + case 8: + color = 'success'; + icon = ; + label = 'Uploaded'; + break; + case 9: + color = 'error'; + icon = ; + label = 'Rejected'; + break; + case 10: + color = 'success'; + icon = ; + label = 'Released'; + break; + default: + color = 'default'; + icon = ; + label = 'Unknown'; + break; + } + + return ( + + ); +}; \ No newline at end of file diff --git a/web/src/app/mapfixes/(styles)/page.scss b/web/src/app/mapfixes/(styles)/page.scss deleted file mode 100644 index daf65f9..0000000 --- a/web/src/app/mapfixes/(styles)/page.scss +++ /dev/null @@ -1,75 +0,0 @@ -@forward "../../_components/styles/mapCard.scss"; - -@use "../../globals.scss"; - -a { - color:rgb(255, 255, 255); - - &:visited, &:hover, &:focus { - text-decoration: none; - color: rgb(255, 255, 255); - } - &:active { - color: rgb(192, 192, 192) - } -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - grid-template-rows: repeat(3, 1fr); - gap: 16px; - max-width: 100%; - margin: 0 auto; - overflow-x: hidden; - box-sizing: border-box; -} - -@media (max-width: 768px) { - .grid { - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - } -} - -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin: 0.3rem; -} - -.pagination button { - padding: 0.25rem 0.5rem; - font-size: 1.15rem; - border: none; - border-radius: 0.35rem; - background-color: #33333350; - color: #fff; - cursor: pointer; -} - -.pagination button:disabled { - background-color: #5555559a; - cursor: not-allowed; -} - -.pagination-dots { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - justify-content: center; - width: 100%; -} - -.dot { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #bbb; - cursor: pointer; -} - -.dot.active { - background-color: #333; -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss deleted file mode 100644 index 4015f24..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page.scss +++ /dev/null @@ -1,19 +0,0 @@ -@forward "./page/commentWindow.scss"; -@forward "./page/reviewStatus.scss"; -@forward "./page/ratingWindow.scss"; -@forward "./page/reviewButtons.scss"; -@forward "./page/comments.scss"; -@forward "./page/review.scss"; -@forward "./page/map.scss"; - -@use "../../../globals.scss"; - -.map-page-main { - display: flex; - justify-content: center; - width: 100vw; -} - -.by-creator { - margin-top: 10px; -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss deleted file mode 100644 index d04b140..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/commentWindow.scss +++ /dev/null @@ -1,56 +0,0 @@ -@use "../../../../globals.scss"; - -#comment-text-field { - @include globals.border-with-radius; - resize: none; - width: 100%; - height: 100px; - background-color: var(--comment-area) -} - -.leave-comment-window { - @include globals.border-with-radius; - width: 100%; - height: 230px; - margin-top: 35px; - - .rating-type { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - gap: 35%; - - .rating-right { - display: grid; - - > span { - margin: 6px 0 6px 0; - } - } - - p { - margin: 15px 0 15px 0; - } - } - - header { - display: flex; - align-items: center; - background-color: var(--window-header); - border-bottom: globals.$review-border; - height: 45px; - - p { - font-weight: bold; - margin: 0 0 0 20px; - } - } - main { - padding: 20px; - - button { - margin-top: 9px; - } - } -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss deleted file mode 100644 index d74b8a6..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/comments.scss +++ /dev/null @@ -1,49 +0,0 @@ -$comments-size: 60px; - -.comments { - display: grid; - gap: 25px; - margin-top: 20px; - - .no-comments { - text-align: center; - margin: 0; - } - - .commenter { - display: flex; - height: $comments-size; - - //BhopMaptest comment - &[data-highlighted="true"] { - background-color: var(--comment-highlighted); - } - > img { - border-radius: 50%; - } - - .name { - font: { - weight: 500; - size: 1.3em; - }; - } - .date { - font-size: .8em; - margin: 0 0 0 5px; - color: #646464 - } - .details { - display: grid; - margin-left: 10px; - - header { - display: flex; - align-items: center; - } - p:not(.date) { - margin: 0; - } - } - } -} diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss deleted file mode 100644 index ede388e..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/map.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use "../../../../globals.scss"; - -.map-image-area { - @include globals.border-with-radius; - display: flex; - justify-content: center; - align-items: center; - width: 350px; - height: 350px; - - > p { - text-align: center; - margin: 0; - } -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss deleted file mode 100644 index 770fe16..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/ratingWindow.scss +++ /dev/null @@ -1,43 +0,0 @@ -@use "../../../../globals.scss"; - -.rating-window { - @include globals.border-with-radius; - width: 100%; - - .rating-type { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - gap: 35%; - - .rating-right { - display: grid; - - > span { - margin: 6px 0 6px 0; - } - } - - p { - margin: 15px 0 15px 0; - } - } - - header { - display: flex; - align-items: center; - background-color: var(--window-header); - border-bottom: globals.$review-border; - height: 45px; - - p { - font-weight: bold; - margin: 0 0 0 20px; - } - } - main { - display: grid; - place-items: center; - } -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss deleted file mode 100644 index 08fc5c0..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/review.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use "../../../../globals.scss"; - -.review-info { - width: 650px; - height: 100%; - - > div { - display: flex; - justify-content: space-between; - align-items: center; - } - p, h1 { - color: var(--text-color); - } - h1 { - font: { - weight: 500; - size: 1.8rem - }; - margin: 0; - } - a { - color: var(--anchor-link-review); - - &:hover { - text-decoration: underline; - } - } -} - -.review-section { - display: flex; - gap: 50px; - margin-top: 20px; -} - -.review-area { - display: grid; - justify-content: center; - gap: 25px; - - img { - width: 100%; - height: 350px; - object-fit: contain - } -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss deleted file mode 100644 index ccc6ecd..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewButtons.scss +++ /dev/null @@ -1,13 +0,0 @@ -@use "../../../../globals.scss"; - -.review-set { - @include globals.border-with-radius; - display: grid; - align-items: center; - gap: 10px; - padding: 10px; - - button { - width: 100%; - } -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss b/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss deleted file mode 100644 index e64bd43..0000000 --- a/web/src/app/mapfixes/[mapfixId]/(styles)/page/reviewStatus.scss +++ /dev/null @@ -1,80 +0,0 @@ -$UnderConstruction: "0"; -$Submitted: "1"; -$ChangesRequested: "2"; -$Accepted: "3"; -$Validating: "4"; -$Validated: "5"; -$Uploading: "6"; -$Uploaded: "7"; -$Rejected: "8"; -$Released: "9"; - -.review-status { - border-radius: 5px; - - p { - margin: 3px 25px 3px 25px; - font-weight: bold; - } - - &[data-review-status="#{$Released}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Rejected}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Uploading}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Uploaded}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Validated}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Validating}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Accepted}"] { - background-color: rgb(2, 162, 2); - p { - color: white; - } - } - &[data-review-status="#{$ChangesRequested}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Submitted}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$UnderConstruction}"] { - background-color: orange; - p { - color: white; - } - } -} diff --git a/web/src/app/mapfixes/[mapfixId]/_comments.tsx b/web/src/app/mapfixes/[mapfixId]/_comments.tsx deleted file mode 100644 index 091092e..0000000 --- a/web/src/app/mapfixes/[mapfixId]/_comments.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { MapfixInfo } from "@/app/ts/Mapfix"; -import { Button } from "@mui/material" -import Window from "./_window"; -import SendIcon from '@mui/icons-material/Send'; -import Image from "next/image"; - -interface CommentersProps { - comments_data: CreatorAndReviewStatus -} - -interface CreatorAndReviewStatus { - asset_id: MapfixInfo["AssetID"], - creator: MapfixInfo["DisplayName"], - review: MapfixInfo["StatusID"], - submitter: MapfixInfo["Submitter"], - target_asset_id: MapfixInfo["TargetAssetID"], - description: MapfixInfo["Description"], - comments: Comment[], - name: string -} - -interface Comment { - picture?: string, //TEMP - comment: string, - date: string, - name: string -} - -function AddComment(comment: Comment) { - const IsBhopMaptest = comment.name == "BhopMaptest" //Highlighted commenter - - return ( -
- {`${comment.name}'s -
-
-

{comment.name}

-

{comment.date}

-
-

{comment.comment}

-
-
- ); -} - -function LeaveAComment() { - return ( - - - - - ) -} - -export function Comments(stats: CommentersProps) { - return (<> -
- {stats.comments_data.comments.length===0 - &&

There are no comments.

- || stats.comments_data.comments.map(comment => ( - - ))} -
- - ) -} - -export { - type CreatorAndReviewStatus, - type Comment, -} diff --git a/web/src/app/mapfixes/[mapfixId]/_mapImage.tsx b/web/src/app/mapfixes/[mapfixId]/_mapImage.tsx deleted file mode 100644 index d82f482..0000000 --- a/web/src/app/mapfixes/[mapfixId]/_mapImage.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from "next/image"; -import { MapfixInfo } from "@/app/ts/Mapfix" - -interface AssetID { - id: MapfixInfo["AssetID"] -} - -function MapImage({ id }: AssetID) { - if (!id) { - return

Missing asset ID

; - } - - const imageUrl = `/thumbnails/asset/${id}`; - - return ( - Map Thumbnail - ); -} - -export { - type AssetID, - MapImage -} diff --git a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx deleted file mode 100644 index 70b93fe..0000000 --- a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Roles, RolesConstants } from "@/app/ts/Roles"; -import { MapfixStatus } from "@/app/ts/Mapfix"; -import { Button, ButtonOwnProps } from "@mui/material"; -import { useState, useEffect } from "react"; - -interface ReviewAction { - name: string, - action: string, -} - -const ReviewActions = { - Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, - BypassSubmit: {name:"Bypass Submit",action:"bypass-submit"} as ReviewAction, - ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction, - Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, - Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, - Reject: {name:"Reject",action:"reject"} as ReviewAction, - Validate: {name:"Validate",action:"retry-validate"} as ReviewAction, - ResetValidating: {name:"Reset Validating (fix softlocked status)",action:"reset-validating"} as ReviewAction, - RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, - Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, - ResetUploading: {name:"Reset Uploading (fix softlocked status)",action:"reset-uploading"} as ReviewAction, -} - -interface ReviewButton { - action: ReviewAction, - mapfixId: string, - color: ButtonOwnProps["color"] -} - -interface ReviewId { - mapfixId: string, - mapfixStatus: number, - mapfixSubmitter: number, -} - -async function ReviewButtonClicked(action: string, mapfixId: string) { - try { - const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, { - method: "POST", - headers: { - "Content-type": "application/json", - } - }); - // Check if the HTTP request was successful - if (!response.ok) { - const errorDetails = await response.text(); - - // Throw an error with detailed information - throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); - } - - window.location.reload(); - } catch (error) { - console.error("Error updating mapfix status:", error); - } -} - -function ReviewButton(props: ReviewButton) { - return -} - -export default function ReviewButtons(props: ReviewId) { - // When is each button visible? - // Multiple buttons can be visible at once. - // Action | Role | When Current Status is One of: - // ---------------|-----------|----------------------- - // Submit | Submitter | UnderConstruction, ChangesRequested - // Revoke | Submitter | Submitted, ChangesRequested - // Accept | Reviewer | Submitted - // Validate | Reviewer | Accepted - // ResetValidating| Reviewer | Validating - // Reject | Reviewer | Submitted - // RequestChanges | Reviewer | Validated, Accepted, Submitted - // Upload | MapAdmin | Validated - // ResetUploading | MapAdmin | Uploading - const { mapfixId, mapfixStatus } = props; - const [user, setUser] = useState(null); - const [roles, setRoles] = useState(RolesConstants.Empty); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function fetchData() { - try { - const [rolesData, userData] = await Promise.all([ - fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()), - fetch("/api/session/user").then(userResponse => userResponse.json()) - ]); - - setRoles(rolesData.Roles); - setUser(userData.UserID); - } catch (error) { - console.error("Error fetching data:", error); - } finally { - setLoading(false); - } - } - - fetchData(); - }, [mapfixId]); - - if (loading) return

Loading...

; - - const visibleButtons: ReviewButton[] = []; - - const is_submitter = user === props.mapfixSubmitter; - if (is_submitter) { - if ([MapfixStatus.UnderConstruction, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) { - visibleButtons.push({ action: ReviewActions.Submit, color: "info", mapfixId }); - } - if ([MapfixStatus.Submitted, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) { - visibleButtons.push({ action: ReviewActions.Revoke, color: "info", mapfixId }); - } - if (mapfixStatus === MapfixStatus.Submitting) { - visibleButtons.push({ action: ReviewActions.ResetSubmitting, color: "error", mapfixId }); - } - } - - if (roles&RolesConstants.MapfixReview) { - // you can force submit a map in ChangesRequested status - if (!is_submitter && mapfixStatus === MapfixStatus.ChangesRequested) { - visibleButtons.push({ action: ReviewActions.BypassSubmit, color: "error", mapfixId }); - } - // you can't review your own mapfix! - // note that this means there needs to be more than one person with MapfixReview - if (!is_submitter && mapfixStatus === MapfixStatus.Submitted) { - visibleButtons.push({ action: ReviewActions.Accept, color: "info", mapfixId }); - visibleButtons.push({ action: ReviewActions.Reject, color: "error", mapfixId }); - } - if (mapfixStatus === MapfixStatus.AcceptedUnvalidated) { - visibleButtons.push({ action: ReviewActions.Validate, color: "info", mapfixId }); - } - if (mapfixStatus === MapfixStatus.Validating) { - visibleButtons.push({ action: ReviewActions.ResetValidating, color: "error", mapfixId }); - } - // this button serves the same purpose as Revoke if you are both - // the map submitter and have MapfixReview when status is Submitted - if ( - [MapfixStatus.Validated, MapfixStatus.AcceptedUnvalidated].includes(mapfixStatus!) - || !is_submitter && mapfixStatus == MapfixStatus.Submitted - ) { - visibleButtons.push({ action: ReviewActions.RequestChanges, color: "error", mapfixId }); - } - } - - if (roles&RolesConstants.MapfixUpload) { - if (mapfixStatus === MapfixStatus.Validated) { - visibleButtons.push({ action: ReviewActions.Upload, color: "info", mapfixId }); - } - if (mapfixStatus === MapfixStatus.Uploading) { - visibleButtons.push({ action: ReviewActions.ResetUploading, color: "error", mapfixId }); - } - } - - return ( -
- {visibleButtons.length === 0 ? ( -

No available actions

- ) : ( - visibleButtons.map((btn) => ( - - )) - )} -
- ); -} diff --git a/web/src/app/mapfixes/[mapfixId]/_window.tsx b/web/src/app/mapfixes/[mapfixId]/_window.tsx deleted file mode 100644 index 866b5a4..0000000 --- a/web/src/app/mapfixes/[mapfixId]/_window.tsx +++ /dev/null @@ -1,20 +0,0 @@ -interface WindowStruct { - className: string, - title: string, - children: React.ReactNode -} - -export default function Window(window: WindowStruct) { - return ( -
-
-

{window.title}

-
-
{window.children}
-
- ) -} - -export { - type WindowStruct -} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx index c7e5047..4af5e69 100644 --- a/web/src/app/mapfixes/[mapfixId]/page.tsx +++ b/web/src/app/mapfixes/[mapfixId]/page.tsx @@ -1,127 +1,420 @@ -"use client" +"use client"; -import { MapfixInfo, MapfixStatusToString } from "@/app/ts/Mapfix"; -import type { CreatorAndReviewStatus } from "./_comments"; -import { MapImage } from "./_mapImage"; -import { useParams } from "next/navigation"; -import ReviewButtons from "./_reviewButtons"; -import { Comments, Comment } from "./_comments"; -import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent"; +import {AuditEvent} from "@/app/ts/AuditEvent"; +import { Roles, RolesConstants } from "@/app/ts/Roles"; import Webpage from "@/app/_components/webpage"; +import { useParams, useRouter } from "next/navigation"; +import {useState, useEffect, useCallback} from "react"; import Link from "next/link"; -import { useState, useEffect } from "react"; -import "./(styles)/page.scss"; +// MUI Components +import { + Typography, + Box, + Container, + Breadcrumbs, + Paper, + Skeleton, + Grid, + CardMedia, + Snackbar, + Alert, +} from "@mui/material"; -interface ReviewId { - mapfixId: string, - mapfixStatus: number, - mapfixSubmitter: number, - mapfixAssetId: number, - mapfixTargetAssetId: number, +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import CommentsAndAuditSection from "@/app/_components/comments/CommentsAndAuditSection"; +import {ReviewItem} from "@/app/_components/review/ReviewItem"; +import {ErrorDisplay} from "@/app/_components/ErrorDisplay"; +import {MapfixInfo} from "@/app/ts/Mapfix"; +import ReviewButtons from "@/app/_components/review/ReviewButtons"; + +// Review action definitions + +interface SnackbarState { + open: boolean; + message: string | null; + severity: 'success' | 'error' | 'info' | 'warning'; } -function RatingArea(mapfix: ReviewId) { - return ( - - ) -} +export default function MapfixDetailsPage() { + const { mapfixId } = useParams<{ mapfixId: string }>(); + const router = useRouter(); -function TitleAndComments(stats: CreatorAndReviewStatus) { - const Review = MapfixStatusToString(stats.review) + const [mapfix, setMapfix] = useState(null); + const [auditEvents, setAuditEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [newComment, setNewComment] = useState(""); + const [user, setUser] = useState(null); + const [roles, setRoles] = useState(RolesConstants.Empty); + const [showBeforeImage, setShowBeforeImage] = useState(false); + const [snackbar, setSnackbar] = useState({ + open: false, + message: null, + severity: 'success' + }); + const showSnackbar = (message: string, severity: 'success' | 'error' | 'info' | 'warning' = 'success') => { + setSnackbar({ + open: true, + message, + severity + }); + }; - // TODO: hide status message when status is not "Accepted" - return ( -
-
-

{stats.name}

- -
-

by {stats.creator}

-

Submitter {stats.submitter}

-

Model Asset ID {stats.asset_id}

-

Target Asset ID {stats.target_asset_id}

-

Description: {stats.description}

- - -
- ) -} + const handleCloseSnackbar = () => { + setSnackbar({ + ...snackbar, + open: false + }); + }; -export default function MapfixInfoPage() { - const { mapfixId } = useParams < { mapfixId: string } >() + const validatorUser = 9223372036854776000; - const [mapfix, setMapfix] = useState(null) - const [auditEvents, setAuditEvents] = useState([]) + const fetchData = useCallback(async (skipLoadingState = false) => { + try { + if (!skipLoadingState) { + setLoading(true); + } + setError(null); - useEffect(() => { // needs to be client sided since server doesn't have a session, nextjs got mad at me for exporting an async function: (https://nextjs.org/docs/messages/no-async-client-component) - async function getMapfix() { - const res = await fetch(`/api/mapfixes/${mapfixId}`) - if (res.ok) { - setMapfix(await res.json()) + const [mapfixData, auditData, rolesData, userData] = await Promise.all([ + fetch(`/api/mapfixes/${mapfixId}`).then(res => { + if (!res.ok) throw new Error(`Failed to fetch mapfix: ${res.status}`); + return res.json(); + }), + fetch(`/api/mapfixes/${mapfixId}/audit-events?Page=1&Limit=100`).then(res => { + if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`); + return res.json(); + }), + fetch("/api/session/roles").then(res => { + if (!res.ok) throw new Error(`Failed to fetch roles: ${res.status}`); + return res.json(); + }), + fetch("/api/session/user").then(res => { + if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`); + return res.json(); + }) + ]); + + setMapfix(mapfixData); + setAuditEvents(auditData); + setRoles(rolesData.Roles); + setUser(userData.UserID); + } catch (error) { + console.error("Error fetching data:", error); + setError(error instanceof Error ? error.message : "Failed to load mapfix details"); + } finally { + if (!skipLoadingState) { + setLoading(false); } } - async function getAuditEvents() { - const res = await fetch(`/api/mapfixes/${mapfixId}/audit-events?Page=1&Limit=100`) - if (res.ok) { - setAuditEvents(await res.json()) + }, [mapfixId]); + + // Fetch mapfix data and audit events + useEffect(() => { + fetchData(); + }, [fetchData]); + + // Handle review button actions + async function handleReviewAction(action: string, mapfixId: number) { + try { + const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, { + method: "POST", + headers: { + "Content-type": "application/json", + } + }); + + if (!response.ok) { + const errorDetails = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); } - } - getMapfix() - getAuditEvents() - }, [mapfixId]) - const comments:Comment[] = auditEvents.map((auditEvent) => { - let username = auditEvent.Username; - if (auditEvent.User == 9223372036854776000) { - username = "[Validator]"; - } - if (username === "" && mapfix && auditEvent.User == mapfix.Submitter) { - username = "[Submitter]"; - } - return { - date: auditEvent.CreatedAt, - name: username, - comment: auditEventMessage(auditEvent), - } - }) + // Set success message based on the action + showSnackbar(`Successfully completed action: ${action}`, "success"); - if (!mapfix) { - return - {/* TODO: Add skeleton loading thingy ? Maybe ? (https://mui.com/material-ui/react-skeleton/) */} - + // Reload data instead of refreshing the page + fetchData(true); + } catch (error) { + console.error("Error updating mapfix status:", error); + showSnackbar(error instanceof Error ? error.message : "Failed to update mapfix", 'error'); + + + // Reload data instead of refreshing the page + fetchData(true); + } } - return ( - -
-
- - -
-
-
- ) -} + + const handleCopyId = (idToCopy: string) => { + navigator.clipboard.writeText(idToCopy); + showSnackbar('ID copied to clipboard', 'success'); + + }; + + const handleCommentSubmit = async () => { + if (!newComment.trim()) { + return; // Don't submit empty comments + } + + try { + const response = await fetch(`/api/mapfixes/${mapfixId}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: newComment, + }); + + if (!response.ok) { + throw new Error(`Failed to post comment: ${response.status}`); + } + + // Clear comment input + setNewComment(""); + + // Refresh audit events to show the new comment + const auditData = await fetch(`/api/mapfixes/${mapfixId}/audit-events?Page=1&Limit=100`); + if (auditData.ok) { + const updatedAuditEvents = await auditData.json(); + setAuditEvents(updatedAuditEvents); + } + } catch (error) { + console.error("Error submitting comment:", error); + setError(error instanceof Error ? error.message : "Failed to submit comment"); + } + }; + + // Loading state + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + if (error || !mapfix) { + return ( + router.push('/mapfixes')} + /> + ); + } + return ( + + + {/* Breadcrumbs Navigation */} + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Mapfixes + + {mapfix.DisplayName} + + + + {/* Left Column - Image and Action Buttons */} + + + + {/* Before/After Images Container */} + + {/* Before Image */} + + + + + {/* After Image */} + + + + + {showBeforeImage ? ( + <> + + BEFORE + + + ) : ( + <> + + AFTER + + + )} + + + + + Click to compare + + + + setShowBeforeImage(!showBeforeImage)} + /> + + + + + + {/* Review Buttons */} + + + + {/* Right Column - Mapfix Details and Comments */} + + + + {/* Comments Section */} + + + + + + + {snackbar.message} + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/mapfixes/page.tsx b/web/src/app/mapfixes/page.tsx index 7eba041..dccdc63 100644 --- a/web/src/app/mapfixes/page.tsx +++ b/web/src/app/mapfixes/page.tsx @@ -2,119 +2,124 @@ import { useState, useEffect } from "react"; import { MapfixList } from "../ts/Mapfix"; -import {MapCard} from "../_components/mapCard"; +import { MapCard } from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; - -// TODO: MAKE MAPFIX & SUBMISSIONS USE THE SAME COMPONENTS :angry: (currently too lazy) - -import "./(styles)/page.scss"; import { ListSortConstants } from "../ts/Sort"; -import {Box, Breadcrumbs, CircularProgress, Container, Pagination, Typography} from "@mui/material"; +import { + Box, + Breadcrumbs, + CircularProgress, + Container, + Pagination, + Typography +} from "@mui/material"; import Link from "next/link"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; export default function MapfixInfoPage() { - const [mapfixes, setMapfixes] = useState(null) + const [mapfixes, setMapfixes] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); - const cardsPerPage = 24; // built to fit on a 1920x1080 monitor + const cardsPerPage = 24; useEffect(() => { const controller = new AbortController(); async function fetchMapFixes() { setIsLoading(true); - const res = await fetch(`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { - signal: controller.signal, - }); - if (res.ok) { - setMapfixes(await res.json()); + try { + const res = await fetch( + `/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, + { signal: controller.signal } + ); + + if (res.ok) { + const data = await res.json(); + setMapfixes(data); + } else { + console.error("Failed to fetch mapfixes:", res.status); + } + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError')) { + console.error("Error fetching mapfixes:", error); + } + } finally { + setIsLoading(false); } - setIsLoading(false); } fetchMapFixes(); - - return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + return () => controller.abort(); }, [currentPage]); if (isLoading || !mapfixes) { - return -
- - - - Loading mapfixes... - - -
-
; + return ( + + + + + + Loading mapfixes... + + + + + ); } const totalPages = Math.ceil(mapfixes.Total / cardsPerPage); - const currentCards = mapfixes.Mapfixes; return ( -
- - - Home + + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home - Mapfixes + Mapfixes + Map Fixes + Explore all submitted fixes for maps from the community. -
- {currentCards.map((submission) => ( + {mapfixes.Mapfixes.map((mapfix) => ( ))} -
- -
+ + + {totalPages > 1 && ( + -
-
-
+
+ )} + - ) -} + ); +} \ No newline at end of file diff --git a/web/src/app/maps/(styles)/page.scss b/web/src/app/maps/(styles)/page.scss deleted file mode 100644 index 052bcda..0000000 --- a/web/src/app/maps/(styles)/page.scss +++ /dev/null @@ -1,43 +0,0 @@ -.maps-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); /* Allows 4 cards per row */ - gap: 20px; - width: 100%; - max-width: 1200px; - padding: 20px; - margin: 0 auto; -} - -.map-card { - background: #1e1e1e; - border-radius: 10px; - overflow: hidden; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - transition: transform 0.2s ease-in-out; - - &:hover { - transform: scale(1.05); - } - - img { - width: 100%; - height: 200px; - object-fit: cover; /* Ensures the image covers the space without being cut off */ - } - - .map-info { - padding: 15px; - text-align: center; - - h2 { - font-size: 1.2rem; - font-weight: bold; - color: #ffffff; - } - - p { - font-size: 1rem; - color: #bbbbbb; - } - } -} diff --git a/web/src/app/maps/[mapId]/_mapImage.tsx b/web/src/app/maps/[mapId]/_mapImage.tsx deleted file mode 100644 index 84bbb10..0000000 --- a/web/src/app/maps/[mapId]/_mapImage.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Image from "next/image"; -import { MapInfo } from "@/app/ts/Map"; - -interface AssetID { - id: MapInfo["ID"]; -} - -function MapImage({ id }: AssetID) { - if (!id) { - return

Missing asset ID

; - } - - const imageUrl = `/thumbnails/asset/${id}`; - - return ( - Map Thumbnail - ); -} - -export { type AssetID, MapImage }; diff --git a/web/src/app/maps/[mapId]/fix/(styles)/page.scss b/web/src/app/maps/[mapId]/fix/(styles)/page.scss deleted file mode 100644 index 8c7dcf8..0000000 --- a/web/src/app/maps/[mapId]/fix/(styles)/page.scss +++ /dev/null @@ -1,54 +0,0 @@ -@use "../../../../globals.scss"; - -::placeholder { - color: var(--placeholder-text) -} - -.form-spacer { - margin-bottom: 20px; - - &:last-of-type { - margin-top: 15px; - } -} - -#target-asset-radio { - color: var(--text-color); - font-size: globals.$form-label-fontsize; -} - -.form-field { - width: 850px; - - & label, & input { - color: var(--text-color); - } - & fieldset { - border-color: rgb(100,100,100); - } - & span { - color: white; - } -} - -main { - display: grid; - justify-content: center; - align-items: center; - margin-inline: auto; - width: 700px; -} - -header h1 { - text-align: center; - color: var(--text-color); -} - -form { - display: grid; - gap: 25px; - - fieldset { - border: blue - } -} diff --git a/web/src/app/maps/[mapId]/fix/page.tsx b/web/src/app/maps/[mapId]/fix/page.tsx index e5e7389..84c5d47 100644 --- a/web/src/app/maps/[mapId]/fix/page.tsx +++ b/web/src/app/maps/[mapId]/fix/page.tsx @@ -1,86 +1,278 @@ "use client" -import { Button, TextField } from "@mui/material" - +import React, { useState, useEffect } from "react"; +import { + Button, + TextField, + Box, + Container, + Typography, + Breadcrumbs, + CircularProgress, + Paper, + Grid, + Alert +} from "@mui/material"; import SendIcon from '@mui/icons-material/Send'; +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import Webpage from "@/app/_components/webpage"; import { useParams } from "next/navigation"; - -import "./(styles)/page.scss" +import Link from "next/link"; +import {MapInfo} from "@/app/ts/Map"; interface MapfixPayload { AssetID: number; TargetAssetID: number; Description: string; } -interface IdResponse { - OperationID: number; -} + +// Game ID mapping +const gameTypes: Record = { + 1: "Bhop", + 2: "Surf", + 5: "Flytrials" +}; export default function MapfixInfoPage() { - const dynamicId = useParams<{ mapId: string }>(); + const { mapId } = useParams<{ mapId: string }>(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [mapDetails, setMapDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + + useEffect(() => { + const fetchMapDetails = async () => { + try { + setIsLoading(true); + const response = await fetch(`/api/maps/${mapId}`); + + if (!response.ok) { + throw new Error(`Failed to fetch map details: ${response.statusText}`); + } + + const data = await response.json(); + setMapDetails(data); + } catch (error) { + console.error("Error fetching map details:", error); + setLoadError(error instanceof Error ? error.message : "Failed to load map details"); + } finally { + setIsLoading(false); + } + }; + + if (mapId) { + fetchMapDetails(); + } + }, [mapId]); + + // Get game type from game ID + const getGameType = (gameId: number | undefined): string => { + if (!gameId) return "Unknown"; + return gameTypes[gameId] || "Unknown"; + }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + setIsSubmitting(true); + setError(null); const form = event.currentTarget; const formData = new FormData(form); + const assetId = formData.get("asset-id") as string; + const description = formData.get("description") as string; + + // Validate required fields + if (!assetId || isNaN(Number(assetId))) { + setError("Please enter a valid Asset ID"); + setIsSubmitting(false); + return; + } + + if (!description) { + setError("Please provide a description for the mapfix"); + setIsSubmitting(false); + return; + } + const payload: MapfixPayload = { - AssetID: Number((formData.get("asset-id") as string) ?? "-1"), - TargetAssetID: Number(dynamicId.mapId), - Description: (formData.get("description") as string) ?? "unknown", // TEMPORARY! TODO: Change + AssetID: Number(assetId), + TargetAssetID: Number(mapId), + Description: description, }; - console.log(payload) - console.log(JSON.stringify(payload)) - try { - // Send the POST request const response = await fetch("/api/mapfixes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); - // Check if the HTTP request was successful if (!response.ok) { const errorDetails = await response.text(); - - // Throw an error with detailed information throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); } - // Allow any HTTP status - const id_response:IdResponse = await response.json(); - - // navigate to newly created mapfix - window.location.assign(`/operations/${id_response.OperationID}`) - + const { OperationID } = await response.json(); + window.location.assign(`/operations/${OperationID}`); } catch (error) { console.error("Error submitting data:", error); + setError(error instanceof Error ? error.message : "An unknown error occurred"); + setIsSubmitting(false); } }; return ( -
-
-

Submit Mapfix

- -
-
- {/* TODO: Add form data for mapfixes, such as changes they did, and any times that need to be deleted & what styles */} - - - - - -
+ + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Maps + + {mapDetails && ( + + {mapDetails.DisplayName} + + )} + Submit Mapfix + + + + + Submit Mapfix + + + Fill out the form below to submit a fix for the selected map + + + + + {loadError && ( + + {loadError} + + )} + + {error && ( + + {error} + + )} + + {isLoading ? ( + + + + ) : ( +
+ + {/* Map details section - disabled prefilled fields */} + + + Map Information + + + + + + + + + + + + + + + + + + + + + + Mapfix Details + + + + + + + + + + + + + + + +
+ )} +
+
- ) -} + ); +} \ No newline at end of file diff --git a/web/src/app/maps/[mapId]/page.tsx b/web/src/app/maps/[mapId]/page.tsx index 6afe389..7892efd 100644 --- a/web/src/app/maps/[mapId]/page.tsx +++ b/web/src/app/maps/[mapId]/page.tsx @@ -1,103 +1,338 @@ -"use client" +"use client"; import { MapInfo } from "@/app/ts/Map"; -import { MapImage } from "./_mapImage"; import Webpage from "@/app/_components/webpage"; -import { useParams } from "next/navigation"; -import { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import React, { useState, useEffect } from "react"; import Link from "next/link"; +import { Snackbar, Alert } from "@mui/material"; // MUI Components import { Typography, Box, - Button as MuiButton, - Card, - CardContent, + Button, + Container, + Breadcrumbs, + Chip, + Grid, + Divider, + Paper, Skeleton, - ThemeProvider, - createTheme, - CssBaseline + Stack, + CardMedia, + Tooltip, + IconButton } from "@mui/material"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; +import PersonIcon from "@mui/icons-material/Person"; +import FlagIcon from "@mui/icons-material/Flag"; +import BugReportIcon from "@mui/icons-material/BugReport"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -interface ButtonProps { - name: string; - href: string; -} - -function Button({ name, href }: ButtonProps) { - return ( - - - {name} - - - ); -} - -const darkTheme = createTheme({ - palette: { - mode: "dark", - }, -}); - -export default function Map() { +export default function MapDetails() { const { mapId } = useParams(); + const router = useRouter(); const [map, setMap] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [copySuccess, setCopySuccess] = useState(false); useEffect(() => { async function getMap() { - const res = await fetch(`/api/maps/${mapId}`); - if (res.ok) { - setMap(await res.json()); + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/maps/${mapId}`); + if (!res.ok) { + throw new Error(`Failed to fetch map: ${res.status}`); + } + const data = await res.json(); + setMap(data); + } catch (error) { + console.error("Error fetching map details:", error); + setError(error instanceof Error ? error.message : "Failed to load map details"); + } finally { + setLoading(false); } } getMap(); }, [mapId]); - return ( - - - - {!map ? ( - - - - - - - - ) : ( - - - - - - Map Info - - Map ID: {mapId} - Display Name: {map.DisplayName} - Creator: {map.Creator} - Game ID: {map.GameID} - Release Date: {new Date(map.Date * 1000).toLocaleString()} - + + + + ); + } + + return ( + + + {/* Breadcrumbs Navigation */} + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Maps + + {loading ? "Loading..." : map?.DisplayName || "Map Details"} + + {loading ? ( + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + map && ( + <> + {/* Map Header */} + + + + {map.DisplayName} + + + {map.GameID && ( + + )} + + + + + + + Created by: {map.Creator} + + + + + + + {formatDate(map.Date)} + + + + + + + + ID: {mapId} + + + handleCopyId(mapId as string)} + sx={{ ml: 1 }} + > + + + + + + + + + + {/* Map Preview Section */} + + + + + + + {/* Map Details Section */} + + + Map Details + + + + + Display Name + {map.DisplayName} + + + + Creator + {map.Creator} + + + + Game Type + {getGameInfo(map.GameID).name} + + + + Release Date + {formatDate(map.Date)} + + + + Map ID + + {mapId} + + handleCopyId(mapId as string)} + sx={{ ml: 1 }} + > + + + + + + + + + + + + + + + ) + )} + + + + Map ID copied to clipboard! + + + - ); -} +} \ No newline at end of file diff --git a/web/src/app/maps/page.tsx b/web/src/app/maps/page.tsx index b93aa66..737e9c9 100644 --- a/web/src/app/maps/page.tsx +++ b/web/src/app/maps/page.tsx @@ -25,6 +25,7 @@ import { } from "@mui/material"; import {Search as SearchIcon} from "@mui/icons-material"; import Link from "next/link"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; interface Map { ID: number; @@ -156,12 +157,15 @@ export default function MapsPage() { - - - Home + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home - Maps + Maps Map Collection diff --git a/web/src/app/operations/[operationId]/(styles)/page.scss b/web/src/app/operations/[operationId]/(styles)/page.scss deleted file mode 100644 index aefa235..0000000 --- a/web/src/app/operations/[operationId]/(styles)/page.scss +++ /dev/null @@ -1,91 +0,0 @@ -.operation-status { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-color: #121212; - color: #e0e0e0; - - .operation-card { - width: 400px; - padding: 20px; - background: #1e1e1e; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); - border-radius: 8px; - - h5 { - margin-bottom: 15px; - font-weight: bold; - color: #ffffff; - } - - p { - margin: 5px 0; - color: #b0b0b0; - } - } - - .status-indicator { - display: flex; - align-items: center; - gap: 8px; - font-weight: bold; - margin-top: 10px; - - &.created { - color: #ffca28; - } - &.completed { - color: #66bb6a; - } - &.failed { - color: #ef5350; - } - - .status-icon { - width: 12px; - height: 12px; - border-radius: 50%; - display: inline-block; - - &.created { - background-color: #ffca28; - } - &.completed { - background-color: #66bb6a; - } - &.failed { - background-color: #ef5350; - } - } - } - - .MuiCircularProgress-root { - color: #90caf9; - } - - .submission-button { - margin-top: 20px; - display: flex; - justify-content: center; - width: 100%; - - button { - width: 100%; - height: 60px; - background-color: #66bb6a; - color: white; - font-size: 20px; - font-weight: bold; - padding: 20px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background 0.3s; - - &:hover { - background-color: #57a05a; - } - } - } -} \ No newline at end of file diff --git a/web/src/app/operations/[operationId]/page.tsx b/web/src/app/operations/[operationId]/page.tsx index 28181a9..050305d 100644 --- a/web/src/app/operations/[operationId]/page.tsx +++ b/web/src/app/operations/[operationId]/page.tsx @@ -1,12 +1,27 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import {useEffect, useState, useRef, ReactElement} from "react"; import { useParams, useRouter } from "next/navigation"; -import { CircularProgress, Typography, Card, CardContent, Button } from "@mui/material"; +import { + CircularProgress, + Typography, + Paper, + Box, + Container, + Button, + Chip, + Divider, + Alert, + Collapse, + IconButton +} from "@mui/material"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import PendingIcon from '@mui/icons-material/Pending'; import Webpage from "@/app/_components/webpage"; -import "./(styles)/page.scss"; - interface Operation { OperationID: number; Status: number; @@ -14,7 +29,7 @@ interface Operation { Owner: string; Date: number; Path: string; - } +} export default function OperationStatusPage() { const router = useRouter(); @@ -23,6 +38,7 @@ export default function OperationStatusPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [operation, setOperation] = useState(null); + const [expandStatusMessage, setExpandStatusMessage] = useState(false); const intervalRef = useRef(null); @@ -78,45 +94,163 @@ export default function OperationStatusPage() { } }; - const getStatusClass = (status: number) => getStatusText(status).toLowerCase(); + const getStatusColor = (status: number) => { + switch (status) { + case 0: + return "warning"; + case 1: + return "success"; + case 2: + return "error"; + default: + return "default"; + } + }; + + const getStatusIcon = (status: number): ReactElement | undefined => { + switch (status) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + default: + return undefined; + } + }; + + // Format the status message for better display + const formatStatusMessage = (message: string) => { + try { + // Check if message is JSON + const parsed = JSON.parse(message); + return JSON.stringify(parsed, null, 2); + } catch { + // Not valid JSON, return as is + return message; + } + }; return ( -
+ + + Operation Status + + {loading ? ( - - ) : error ? ( - {error} - ) : operation ? ( - - - Operation ID: {operation.OperationID} -
- - {getStatusText(operation.Status)} -
- Status Message: {operation.StatusMessage} - Owner: {operation.Owner} - Date: {new Date(operation.Date * 1000).toLocaleString()} - Path: {operation.Path} + + + + Loading operation details... + + + ) : error ? ( + + {error} + + ) : operation ? ( + + + + Operation #{operation.OperationID} + + + + + + + + + Owner: {operation.Owner} + + + Date: {new Date(operation.Date * 1000).toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} + + + + + + + Status Message + + setExpandStatusMessage(!expandStatusMessage)} + aria-label={expandStatusMessage ? "Collapse details" : "Expand details"} + > + {expandStatusMessage ? : } + + + + + +
+                    {formatStatusMessage(operation.StatusMessage)}
+                  
+
+
+ + {!expandStatusMessage && ( + + {operation.StatusMessage.length > 100 + ? `${operation.StatusMessage.substring(0, 100)}...` + : operation.StatusMessage} + + )} +
{operation.Status === 1 && ( -
- -
+ + + )} -
-
- ) : ( - No operation found. + + ) : ( + + No operation found with ID: {operationId} + )} -
+
); -} +} \ No newline at end of file diff --git a/web/src/app/submissions/(styles)/page.scss b/web/src/app/submissions/(styles)/page.scss deleted file mode 100644 index d54fd38..0000000 --- a/web/src/app/submissions/(styles)/page.scss +++ /dev/null @@ -1,75 +0,0 @@ -@forward "../../_components/styles/mapCard.scss"; - -@use "../../globals.scss"; - -a { - color:rgb(255, 255, 255); - - &:visited, &:hover, &:focus { - text-decoration: none; - color: rgb(255, 255, 255); - } - &:active { - color: rgb(192, 192, 192) - } -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - grid-template-rows: repeat(3, 1fr); - gap: 16px; - max-width: 100%; - margin: 0 auto; - overflow-x: hidden; - box-sizing: border-box; -} - -@media (max-width: 768px) { - .grid { - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - } -} - -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin: 0.3rem; -} - -.pagination button { - padding: 0.25rem 0.5rem; - font-size: 1.15rem; - border: none; - border-radius: 0.35rem; - background-color: #33333350; - color: #fff; - cursor: pointer; -} - -.pagination button:disabled { - background-color: #5555559a; - cursor: not-allowed; -} - -.pagination-dots { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - justify-content: center; - width: 100%; -} - -.dot { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #bbb; - cursor: pointer; -} - -.dot.active { - background-color: #333; -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page.scss b/web/src/app/submissions/[submissionId]/(styles)/page.scss deleted file mode 100644 index 4015f24..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page.scss +++ /dev/null @@ -1,19 +0,0 @@ -@forward "./page/commentWindow.scss"; -@forward "./page/reviewStatus.scss"; -@forward "./page/ratingWindow.scss"; -@forward "./page/reviewButtons.scss"; -@forward "./page/comments.scss"; -@forward "./page/review.scss"; -@forward "./page/map.scss"; - -@use "../../../globals.scss"; - -.map-page-main { - display: flex; - justify-content: center; - width: 100vw; -} - -.by-creator { - margin-top: 10px; -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/commentWindow.scss b/web/src/app/submissions/[submissionId]/(styles)/page/commentWindow.scss deleted file mode 100644 index d04b140..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/commentWindow.scss +++ /dev/null @@ -1,56 +0,0 @@ -@use "../../../../globals.scss"; - -#comment-text-field { - @include globals.border-with-radius; - resize: none; - width: 100%; - height: 100px; - background-color: var(--comment-area) -} - -.leave-comment-window { - @include globals.border-with-radius; - width: 100%; - height: 230px; - margin-top: 35px; - - .rating-type { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - gap: 35%; - - .rating-right { - display: grid; - - > span { - margin: 6px 0 6px 0; - } - } - - p { - margin: 15px 0 15px 0; - } - } - - header { - display: flex; - align-items: center; - background-color: var(--window-header); - border-bottom: globals.$review-border; - height: 45px; - - p { - font-weight: bold; - margin: 0 0 0 20px; - } - } - main { - padding: 20px; - - button { - margin-top: 9px; - } - } -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/comments.scss b/web/src/app/submissions/[submissionId]/(styles)/page/comments.scss deleted file mode 100644 index d74b8a6..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/comments.scss +++ /dev/null @@ -1,49 +0,0 @@ -$comments-size: 60px; - -.comments { - display: grid; - gap: 25px; - margin-top: 20px; - - .no-comments { - text-align: center; - margin: 0; - } - - .commenter { - display: flex; - height: $comments-size; - - //BhopMaptest comment - &[data-highlighted="true"] { - background-color: var(--comment-highlighted); - } - > img { - border-radius: 50%; - } - - .name { - font: { - weight: 500; - size: 1.3em; - }; - } - .date { - font-size: .8em; - margin: 0 0 0 5px; - color: #646464 - } - .details { - display: grid; - margin-left: 10px; - - header { - display: flex; - align-items: center; - } - p:not(.date) { - margin: 0; - } - } - } -} diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/map.scss b/web/src/app/submissions/[submissionId]/(styles)/page/map.scss deleted file mode 100644 index 12469f0..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/map.scss +++ /dev/null @@ -1,19 +0,0 @@ -@use "../../../../globals.scss"; - -.map-image-area { - @include globals.border-with-radius; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: auto; - margin-left: auto; - margin-right: auto; - border-radius: 12px; - overflow: hidden; - - > p { - text-align: center; - margin: 0; - } -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/ratingWindow.scss b/web/src/app/submissions/[submissionId]/(styles)/page/ratingWindow.scss deleted file mode 100644 index 770fe16..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/ratingWindow.scss +++ /dev/null @@ -1,43 +0,0 @@ -@use "../../../../globals.scss"; - -.rating-window { - @include globals.border-with-radius; - width: 100%; - - .rating-type { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - gap: 35%; - - .rating-right { - display: grid; - - > span { - margin: 6px 0 6px 0; - } - } - - p { - margin: 15px 0 15px 0; - } - } - - header { - display: flex; - align-items: center; - background-color: var(--window-header); - border-bottom: globals.$review-border; - height: 45px; - - p { - font-weight: bold; - margin: 0 0 0 20px; - } - } - main { - display: grid; - place-items: center; - } -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/review.scss b/web/src/app/submissions/[submissionId]/(styles)/page/review.scss deleted file mode 100644 index 2ca4539..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/review.scss +++ /dev/null @@ -1,46 +0,0 @@ -@use "../../../../globals.scss"; - -.review-info { - width: 650px; - height: 100%; - - > div { - display: flex; - justify-content: space-between; - align-items: center; - } - p, h1 { - color: var(--text-color); - } - h1 { - font: { - weight: 500; - size: 1.8rem - }; - margin: 0; - } - a { - color: var(--anchor-link-review); - - &:hover { - text-decoration: underline; - } - } -} - -.review-section { - display: flex; - gap: 50px; - margin-top: 20px; -} - -.review-area { - display: grid; - justify-content: center; - gap: 25px; - - img { - height: 100%; - object-fit: contain - } -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/reviewButtons.scss b/web/src/app/submissions/[submissionId]/(styles)/page/reviewButtons.scss deleted file mode 100644 index ccc6ecd..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/reviewButtons.scss +++ /dev/null @@ -1,13 +0,0 @@ -@use "../../../../globals.scss"; - -.review-set { - @include globals.border-with-radius; - display: grid; - align-items: center; - gap: 10px; - padding: 10px; - - button { - width: 100%; - } -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/(styles)/page/reviewStatus.scss b/web/src/app/submissions/[submissionId]/(styles)/page/reviewStatus.scss deleted file mode 100644 index e64bd43..0000000 --- a/web/src/app/submissions/[submissionId]/(styles)/page/reviewStatus.scss +++ /dev/null @@ -1,80 +0,0 @@ -$UnderConstruction: "0"; -$Submitted: "1"; -$ChangesRequested: "2"; -$Accepted: "3"; -$Validating: "4"; -$Validated: "5"; -$Uploading: "6"; -$Uploaded: "7"; -$Rejected: "8"; -$Released: "9"; - -.review-status { - border-radius: 5px; - - p { - margin: 3px 25px 3px 25px; - font-weight: bold; - } - - &[data-review-status="#{$Released}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Rejected}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Uploading}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Uploaded}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Validated}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Validating}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Accepted}"] { - background-color: rgb(2, 162, 2); - p { - color: white; - } - } - &[data-review-status="#{$ChangesRequested}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$Submitted}"] { - background-color: orange; - p { - color: white; - } - } - &[data-review-status="#{$UnderConstruction}"] { - background-color: orange; - p { - color: white; - } - } -} diff --git a/web/src/app/submissions/[submissionId]/_comments.tsx b/web/src/app/submissions/[submissionId]/_comments.tsx deleted file mode 100644 index 722194c..0000000 --- a/web/src/app/submissions/[submissionId]/_comments.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { SubmissionInfo } from "@/app/ts/Submission"; -import { Button } from "@mui/material" -import Window from "./_window"; -import SendIcon from '@mui/icons-material/Send'; -import Image from "next/image"; - -interface CommentersProps { - comments_data: CreatorAndReviewStatus -} - -interface CreatorAndReviewStatus { - asset_id: SubmissionInfo["AssetID"], - creator: SubmissionInfo["DisplayName"], - review: SubmissionInfo["StatusID"], - submitter: SubmissionInfo["Submitter"], - uploaded_asset_id: SubmissionInfo["UploadedAssetID"], - comments: Comment[], - name: string -} - -interface Comment { - picture?: string, //TEMP - comment: string, - date: string, - name: string -} - -function AddComment(comment: Comment) { - const IsBhopMaptest = comment.name == "BhopMaptest" //Highlighted commenter - - return ( -
- {`${comment.name}'s -
-
-

{comment.name}

-

{comment.date}

-
-

{comment.comment}

-
-
- ); -} - -function LeaveAComment() { - return ( - - - - - ) -} - -export function Comments(stats: CommentersProps) { - return (<> -
- {stats.comments_data.comments.length===0 - &&

There are no comments.

- || stats.comments_data.comments.map(comment => ( - - ))} -
- - ) -} - -export { - type CreatorAndReviewStatus, - type Comment, -} diff --git a/web/src/app/submissions/[submissionId]/_mapImage.tsx b/web/src/app/submissions/[submissionId]/_mapImage.tsx deleted file mode 100644 index 96f9059..0000000 --- a/web/src/app/submissions/[submissionId]/_mapImage.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Image from "next/image"; -import { SubmissionInfo } from "@/app/ts/Submission"; - -interface AssetID { - id: SubmissionInfo["AssetID"]; -} - -function MapImage({ id }: AssetID) { - if (!id) { - return

Missing asset ID

; - } - - const imageUrl = `/thumbnails/asset/${id}`; - - return ( - Map Thumbnail - ); -} - -export { type AssetID, MapImage }; diff --git a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx b/web/src/app/submissions/[submissionId]/_reviewButtons.tsx deleted file mode 100644 index b388cf1..0000000 --- a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { Roles, RolesConstants } from "@/app/ts/Roles"; -import { SubmissionStatus } from "@/app/ts/Submission"; -import { Button, ButtonOwnProps } from "@mui/material"; -import { useState, useEffect } from "react"; - -interface ReviewAction { - name: string, - action: string, -} - -const ReviewActions = { - Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, - AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction, - BypassSubmit: {name:"Bypass Submit",action:"bypass-submit"} as ReviewAction, - ResetSubmitting: {name:"Reset Submitting (fix softlocked status)",action:"reset-submitting"} as ReviewAction, - Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, - Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, - Reject: {name:"Reject",action:"reject"} as ReviewAction, - Validate: {name:"Validate",action:"retry-validate"} as ReviewAction, - ResetValidating: {name:"Reset Validating (fix softlocked status)",action:"reset-validating"} as ReviewAction, - RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, - Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, - ResetUploading: {name:"Reset Uploading (fix softlocked status)",action:"reset-uploading"} as ReviewAction, -} - -interface ReviewButton { - action: ReviewAction, - submissionId: string, - color: ButtonOwnProps["color"] -} - -interface ReviewId { - submissionId: string, - submissionStatus: number, - submissionSubmitter: number, -} - -async function ReviewButtonClicked(action: string, submissionId: string) { - try { - const response = await fetch(`/api/submissions/${submissionId}/status/${action}`, { - method: "POST", - headers: { - "Content-type": "application/json", - } - }); - // Check if the HTTP request was successful - if (!response.ok) { - const errorDetails = await response.text(); - - // Throw an error with detailed information - throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); - } - - window.location.reload(); - } catch (error) { - console.error("Error updating submission status:", error); - } -} - -function ReviewButton(props: ReviewButton) { - return -} - -export default function ReviewButtons(props: ReviewId) { - // When is each button visible? - // Multiple buttons can be visible at once. - // Action | Role | When Current Status is One of: - // ---------------|-----------|----------------------- - // Submit | Submitter | UnderConstruction, ChangesRequested - // Revoke | Submitter | Submitted, ChangesRequested - // Accept | Reviewer | Submitted - // Validate | Reviewer | Accepted - // ResetValidating| Reviewer | Validating - // Reject | Reviewer | Submitted - // RequestChanges | Reviewer | Validated, Accepted, Submitted - // Upload | MapAdmin | Validated - // ResetUploading | MapAdmin | Uploading - const { submissionId, submissionStatus } = props; - const [user, setUser] = useState(null); - const [roles, setRoles] = useState(RolesConstants.Empty); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function fetchData() { - try { - const [rolesData, userData] = await Promise.all([ - fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()), - fetch("/api/session/user").then(userResponse => userResponse.json()) - ]); - - setRoles(rolesData.Roles); - setUser(userData.UserID); - } catch (error) { - console.error("Error fetching data:", error); - } finally { - setLoading(false); - } - } - - fetchData(); - }, [submissionId]); - - if (loading) return

Loading...

; - - const visibleButtons: ReviewButton[] = []; - - const is_submitter = user === props.submissionSubmitter; - if (is_submitter) { - if ([SubmissionStatus.UnderConstruction, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) { - visibleButtons.push({ action: ReviewActions.Submit, color: "info", submissionId }); - } - if ([SubmissionStatus.Submitted, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) { - visibleButtons.push({ action: ReviewActions.Revoke, color: "info", submissionId }); - } - if (submissionStatus === SubmissionStatus.Submitting) { - visibleButtons.push({ action: ReviewActions.ResetSubmitting, color: "error", submissionId }); - } - } - - if (roles&RolesConstants.SubmissionReview) { - // you can force submit a map in ChangesRequested status - if (!is_submitter && submissionStatus === SubmissionStatus.ChangesRequested) { - visibleButtons.push({ action: ReviewActions.AdminSubmit, color: "error", submissionId }); - visibleButtons.push({ action: ReviewActions.BypassSubmit, color: "error", submissionId }); - } - // you can't review your own submission! - // note that this means there needs to be more than one person with SubmissionReview - if (!is_submitter && submissionStatus === SubmissionStatus.Submitted) { - visibleButtons.push({ action: ReviewActions.Accept, color: "info", submissionId }); - visibleButtons.push({ action: ReviewActions.Reject, color: "error", submissionId }); - } - if (submissionStatus === SubmissionStatus.AcceptedUnvalidated) { - visibleButtons.push({ action: ReviewActions.Validate, color: "info", submissionId }); - } - if (submissionStatus === SubmissionStatus.Validating) { - visibleButtons.push({ action: ReviewActions.ResetValidating, color: "error", submissionId }); - } - // this button serves the same purpose as Revoke if you are both - // the map submitter and have SubmissionReview when status is Submitted - if ( - [SubmissionStatus.Validated, SubmissionStatus.AcceptedUnvalidated].includes(submissionStatus!) - || !is_submitter && submissionStatus == SubmissionStatus.Submitted - ) { - visibleButtons.push({ action: ReviewActions.RequestChanges, color: "error", submissionId }); - } - } - - if (roles&RolesConstants.SubmissionUpload) { - if (submissionStatus === SubmissionStatus.Validated) { - visibleButtons.push({ action: ReviewActions.Upload, color: "info", submissionId }); - } - // TODO: hide Reset buttons for 10 seconds - if (submissionStatus === SubmissionStatus.Uploading) { - visibleButtons.push({ action: ReviewActions.ResetUploading, color: "error", submissionId }); - } - } - - return ( -
- {visibleButtons.length === 0 ? ( -

No available actions

- ) : ( - visibleButtons.map((btn) => ( - - )) - )} -
- ); -} diff --git a/web/src/app/submissions/[submissionId]/_window.tsx b/web/src/app/submissions/[submissionId]/_window.tsx deleted file mode 100644 index 866b5a4..0000000 --- a/web/src/app/submissions/[submissionId]/_window.tsx +++ /dev/null @@ -1,20 +0,0 @@ -interface WindowStruct { - className: string, - title: string, - children: React.ReactNode -} - -export default function Window(window: WindowStruct) { - return ( -
-
-

{window.title}

-
-
{window.children}
-
- ) -} - -export { - type WindowStruct -} \ No newline at end of file diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index e2097d5..ec202a3 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -1,109 +1,316 @@ -"use client" +"use client"; -import { SubmissionInfo, SubmissionStatusToString } from "@/app/ts/Submission"; -import type { CreatorAndReviewStatus } from "./_comments"; -import { MapImage } from "./_mapImage"; -import { useParams } from "next/navigation"; -import ReviewButtons from "./_reviewButtons"; -import { Comments, Comment } from "./_comments"; -import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent"; +import { SubmissionInfo } from "@/app/ts/Submission"; +import {AuditEvent} from "@/app/ts/AuditEvent"; +import { Roles, RolesConstants } from "@/app/ts/Roles"; import Webpage from "@/app/_components/webpage"; +import { useParams, useRouter } from "next/navigation"; +import {useState, useEffect, useCallback} from "react"; import Link from "next/link"; -import { useState, useEffect } from "react"; -import "./(styles)/page.scss"; +// MUI Components +import { + Typography, + Box, + Container, + Breadcrumbs, + Paper, + Skeleton, + Grid, + CardMedia, + Snackbar, + Alert, +} from "@mui/material"; -interface ReviewId { - submissionId: string; - assetId: number; - submissionStatus: number; - submissionSubmitter: number, +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import CommentsAndAuditSection from "@/app/_components/comments/CommentsAndAuditSection"; +import {ReviewItem} from "@/app/_components/review/ReviewItem"; +import {ErrorDisplay} from "@/app/_components/ErrorDisplay"; +import ReviewButtons from "@/app/_components/review/ReviewButtons"; + +interface SnackbarState { + open: boolean; + message: string | null; + severity: 'success' | 'error' | 'info' | 'warning'; } -function RatingArea(submission: ReviewId) { - return ( - - ) -} +export default function SubmissionDetailsPage() { + const { submissionId } = useParams<{ submissionId: string }>(); + const router = useRouter(); -function TitleAndComments(stats: CreatorAndReviewStatus) { - const Review = SubmissionStatusToString(stats.review) + const [submission, setSubmission] = useState(null); + const [auditEvents, setAuditEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [newComment, setNewComment] = useState(""); + const [user, setUser] = useState(null); + const [roles, setRoles] = useState(RolesConstants.Empty); + const [snackbar, setSnackbar] = useState({ + open: false, + message: null, + severity: 'success' + }); + const showSnackbar = (message: string, severity: 'success' | 'error' | 'info' | 'warning' = 'success') => { + setSnackbar({ + open: true, + message, + severity + }); + }; - // TODO: hide status message when status is not "Accepted" - return ( -
-
-

{stats.name}

- -
-

by {stats.creator}

-

Submitter {stats.submitter}

-

Model Asset ID {stats.asset_id}

-

Uploaded Asset ID {stats.uploaded_asset_id}

- - -
- ) -} + const handleCloseSnackbar = () => { + setSnackbar({ + ...snackbar, + open: false + }); + }; -export default function SubmissionInfoPage() { - const { submissionId } = useParams < { submissionId: string } >() - const [submission, setSubmission] = useState(null) - const [auditEvents, setAuditEvents] = useState([]) + const validatorUser = 9223372036854776000; - useEffect(() => { // needs to be client sided since server doesn't have a session, nextjs got mad at me for exporting an async function: (https://nextjs.org/docs/messages/no-async-client-component) - async function getSubmission() { - const res = await fetch(`/api/submissions/${submissionId}`) - if (res.ok) { - setSubmission(await res.json()) + const fetchData = useCallback(async (skipLoadingState = false) => { + try { + if (!skipLoadingState) { + setLoading(true); + } + setError(null); + + const [submissionData, auditData, rolesData, userData] = await Promise.all([ + fetch(`/api/submissions/${submissionId}`).then(res => { + if (!res.ok) throw new Error(`Failed to fetch submission: ${res.status}`); + return res.json(); + }), + fetch(`/api/submissions/${submissionId}/audit-events?Page=1&Limit=100`).then(res => { + if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`); + return res.json(); + }), + fetch("/api/session/roles").then(res => { + if (!res.ok) throw new Error(`Failed to fetch roles: ${res.status}`); + return res.json(); + }), + fetch("/api/session/user").then(res => { + if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`); + return res.json(); + }) + ]); + + setSubmission(submissionData); + setAuditEvents(auditData); + setRoles(rolesData.Roles); + setUser(userData.UserID); + } catch (error) { + console.error("Error fetching data:", error); + setError(error instanceof Error ? error.message : "Failed to load submission details"); + } finally { + if (!skipLoadingState) { + setLoading(false); } } - async function getAuditEvents() { - const res = await fetch(`/api/submissions/${submissionId}/audit-events?Page=1&Limit=100`) - if (res.ok) { - setAuditEvents(await res.json()) + }, [submissionId]); + + // Fetch submission data and audit events + useEffect(() => { + fetchData(); + }, [fetchData]); + + // Handle review button actions + async function handleReviewAction(action: string, submissionId: number) { + try { + const response = await fetch(`/api/submissions/${submissionId}/status/${action}`, { + method: "POST", + headers: { + "Content-type": "application/json", + } + }); + + if (!response.ok) { + const errorDetails = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); } - } - getSubmission() - getAuditEvents() - }, [submissionId]) - const comments:Comment[] = auditEvents.map((auditEvent) => { - let username = auditEvent.Username; - if (auditEvent.User == 9223372036854776000) { - username = "[Validator]"; - } - if (username === "" && submission && auditEvent.User == submission.Submitter) { - username = "[Submitter]"; - } - return { - date: auditEvent.CreatedAt, - name: username, - comment: auditEventMessage(auditEvent), - } - }) + // Set success message based on the action + showSnackbar(`Successfully completed action: ${action}`, "success"); - if (!submission) { - return - {/* TODO: Add skeleton loading thingy ? Maybe ? (https://mui.com/material-ui/react-skeleton/) */} - + // Reload data instead of refreshing the page + fetchData(true); + } catch (error) { + console.error("Error updating submission status:", error); + showSnackbar(error instanceof Error ? error.message : "Failed to update submission", 'error'); + + + // Reload data instead of refreshing the page + fetchData(true); + } } - return ( - -
-
- - -
-
-
- ) -} + + const handleCopyId = (idToCopy: string) => { + navigator.clipboard.writeText(idToCopy); + showSnackbar('ID copied to clipboard', 'success'); + + }; + + const handleCommentSubmit = async () => { + if (!newComment.trim()) { + return; // Don't submit empty comments + } + + try { + const response = await fetch(`/api/submissions/${submissionId}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: newComment, + }); + + if (!response.ok) { + throw new Error(`Failed to post comment: ${response.status}`); + } + + // Clear comment input + setNewComment(""); + + // Refresh audit events to show the new comment + const auditData = await fetch(`/api/submissions/${submissionId}/audit-events?Page=1&Limit=100`); + if (auditData.ok) { + const updatedAuditEvents = await auditData.json(); + setAuditEvents(updatedAuditEvents); + } + } catch (error) { + console.error("Error submitting comment:", error); + setError(error instanceof Error ? error.message : "Failed to submit comment"); + } + }; + + // Loading state + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + if (error || !submission) { + return ( + router.push('/submissions')} + /> + ); + } + return ( + + + {/* Breadcrumbs Navigation */} + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Submissions + + {submission.DisplayName} + + + + {/* Left Column - Image and Action Buttons */} + + + {submission.AssetID ? ( + + ) : ( + + No image available + + )} + + + {/* Review Buttons */} + + + + {/* Right Column - Submission Details and Comments */} + + + + {/* Comments Section */} + + + + + + + {snackbar.message} + + + + + ); +} \ No newline at end of file diff --git a/web/src/app/submissions/page.tsx b/web/src/app/submissions/page.tsx index c81e090..4c226f9 100644 --- a/web/src/app/submissions/page.tsx +++ b/web/src/app/submissions/page.tsx @@ -1,109 +1,119 @@ 'use client' -import {useState, useEffect} from "react"; -import {SubmissionList} from "../ts/Submission"; -import {MapCard} from "../_components/mapCard"; +import { useState, useEffect } from "react"; +import { SubmissionList } from "../ts/Submission"; +import { MapCard } from "../_components/mapCard"; import Webpage from "@/app/_components/webpage"; - -import "./(styles)/page.scss"; -import {ListSortConstants} from "../ts/Sort"; -import {Breadcrumbs, Pagination, Typography, CircularProgress, Box, Container} from "@mui/material"; +import { ListSortConstants } from "../ts/Sort"; +import { + Box, + Breadcrumbs, + CircularProgress, + Container, + Pagination, + Typography +} from "@mui/material"; import Link from "next/link"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; export default function SubmissionInfoPage() { const [submissions, setSubmissions] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); - const cardsPerPage = 24; // built to fit on a 1920x1080 monitor + const cardsPerPage = 24; useEffect(() => { const controller = new AbortController(); async function fetchSubmissions() { setIsLoading(true); - const res = await fetch(`/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, { - signal: controller.signal, - }); - if (res.ok) { - setSubmissions(await res.json()); + try { + const res = await fetch( + `/api/submissions?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`, + { signal: controller.signal } + ); + + if (res.ok) { + const data = await res.json(); + setSubmissions(data); + } else { + console.error("Failed to fetch submissions:", res.status); + } + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError')) { + console.error("Error fetching submissions:", error); + } + } finally { + setIsLoading(false); } - setIsLoading(false); } fetchSubmissions(); - - return () => controller.abort(); // Cleanup to avoid fetch conflicts on rapid page changes + return () => controller.abort(); }, [currentPage]); if (isLoading || !submissions) { - return -
- - - - Loading submissions... - - -
-
; + return ( + + + + + + Loading submissions... + + + + + ); } const totalPages = Math.ceil(submissions.Total / cardsPerPage); - const currentCards = submissions.Submissions; if (submissions.Total === 0) { - return -
- Submissions list is empty. -
-
; + return ( + + + + Submissions list is empty. + + + + ); } return ( -
- - - Home + + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home - Submissions + Submissions + Submissions + Explore all submitted maps from the community. -
- {currentCards.map((submission) => ( + {submissions.Submissions.map((submission) => ( ))} -
- -
+ + + {totalPages > 1 && ( + -
-
-
+ + )} +
- ) -} + ); +} \ No newline at end of file diff --git a/web/src/app/submit/(styles)/page.scss b/web/src/app/submit/(styles)/page.scss deleted file mode 100644 index 23bf59a..0000000 --- a/web/src/app/submit/(styles)/page.scss +++ /dev/null @@ -1,54 +0,0 @@ -@use "../../globals.scss"; - -::placeholder { - color: var(--placeholder-text) -} - -.form-spacer { - margin-bottom: 20px; - - &:last-of-type { - margin-top: 15px; - } -} - -#target-asset-radio { - color: var(--text-color); - font-size: globals.$form-label-fontsize; -} - -.form-field { - width: 850px; - - & label, & input { - color: var(--text-color); - } - & fieldset { - border-color: rgb(100,100,100); - } - & span { - color: white; - } -} - -main { - display: grid; - justify-content: center; - align-items: center; - margin-inline: auto; - width: 700px; -} - -header h1 { - text-align: center; - color: var(--text-color); -} - -form { - display: grid; - gap: 25px; - - fieldset { - border: blue - } -} diff --git a/web/src/app/submit/page.tsx b/web/src/app/submit/page.tsx index 99afdae..51c2963 100644 --- a/web/src/app/submit/page.tsx +++ b/web/src/app/submit/page.tsx @@ -1,13 +1,23 @@ "use client" -import { Button, TextField } from "@mui/material" - -import GameSelection from "./_game"; -import SendIcon from '@mui/icons-material/Send'; -import Webpage from "@/app/_components/webpage" import React, { useState } from "react"; - -import "./(styles)/page.scss" +import { + Button, + TextField, + Box, + Container, + Typography, + Breadcrumbs, + CircularProgress, + Paper, + Grid, + FormControl +} from "@mui/material"; +import SendIcon from '@mui/icons-material/Send'; +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import Webpage from "@/app/_components/webpage"; +import GameSelection from "./_game"; +import Link from "next/link"; interface SubmissionPayload { AssetID: number; @@ -15,76 +25,156 @@ interface SubmissionPayload { Creator: string; GameID: number; } -interface IdResponse { - OperationID: number; -} -export default function SubmissionInfoPage() { +export default function SubmitPage() { const [game, setGame] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + setIsSubmitting(true); + setError(null); const form = event.currentTarget; const formData = new FormData(form); + const assetId = formData.get("asset-id") as string; + const displayName = formData.get("display-name") as string; + const creator = formData.get("creator") as string; + + // Validate required fields + if (!assetId || isNaN(Number(assetId))) { + setError("Please enter a valid Asset ID"); + setIsSubmitting(false); + return; + } + const payload: SubmissionPayload = { - DisplayName: (formData.get("display-name") as string) ?? "unknown", // TEMPORARY! TODO: Change - Creator: (formData.get("creator") as string) ?? "unknown", // TEMPORARY! TODO: Change + AssetID: Number(assetId), + DisplayName: displayName || "unknown", + Creator: creator || "unknown", GameID: game, - AssetID: Number((formData.get("asset-id") as string) ?? "-1"), }; - console.log(payload) - console.log(JSON.stringify(payload)) - try { - // Send the POST request const response = await fetch("/api/submissions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); - // Check if the HTTP request was successful if (!response.ok) { const errorDetails = await response.text(); - - // Throw an error with detailed information throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`); } - // Allow any HTTP status - const id_response:IdResponse = await response.json(); - - // navigate to newly created submission - window.location.assign(`/operations/${id_response.OperationID}`) - + const { OperationID } = await response.json(); + window.location.assign(`/operations/${OperationID}`); } catch (error) { console.error("Error submitting data:", error); + setError(error instanceof Error ? error.message : "An unknown error occurred"); + setIsSubmitting(false); } }; return ( -
-
-

Submit New Map

- -
-
- - - - - - - -
+ + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Maps + + Submit Map + + + + Submit New Map + + + Fill out the form below to submit a new map to the game + + + + {error && ( + + {error} + + )} + +
+ + + + + + + + + + + + + + + + + Select Game Type + + + + + + + + + +
+
+
- ) -} + ); +} \ No newline at end of file diff --git a/web/src/app/ts/AuditEvent.ts b/web/src/app/ts/AuditEvent.ts index f9882a1..d7b506f 100644 --- a/web/src/app/ts/AuditEvent.ts +++ b/web/src/app/ts/AuditEvent.ts @@ -54,7 +54,7 @@ export interface AuditEventDataError { // Full audit event type (mirroring the Go struct) export interface AuditEvent { Id: number; - CreatedAt: string; // ISO string, can convert to Date if needed + Date: number; User: number; Username: string; ResourceType: string; // Assuming this is a string enum or similar diff --git a/web/src/app/ts/Submission.ts b/web/src/app/ts/Submission.ts index 89d9cd8..4d7b18d 100644 --- a/web/src/app/ts/Submission.ts +++ b/web/src/app/ts/Submission.ts @@ -48,11 +48,11 @@ function SubmissionStatusToString(submission_status: SubmissionStatus): string { case SubmissionStatus.Validating: return "VALIDATING" case SubmissionStatus.AcceptedUnvalidated: - return "ACCEPTED, NOT VALIDATED" + return "SCRIPT REVIEW" case SubmissionStatus.ChangesRequested: return "CHANGES REQUESTED" case SubmissionStatus.Submitted: - return "SUBMITTED" + return "UNDER REVIEW" case SubmissionStatus.Submitting: return "SUBMITTING" case SubmissionStatus.UnderConstruction: -- 2.49.1 From b60d2b6186ce9fe0673f79e74ad5577c24ed4c0a Mon Sep 17 00:00:00 2001 From: Quaternions Date: Mon, 9 Jun 2025 01:03:55 +0000 Subject: [PATCH 11/11] Submissions: Fix Comments (#184) Closes #163. Reviewed-on: https://git.itzana.me/StrafesNET/maps-service/pulls/184 Reviewed-by: itzaname Co-authored-by: Quaternions Co-committed-by: Quaternions --- pkg/service/audit_events.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pkg/service/audit_events.go b/pkg/service/audit_events.go index 1dfffda..8dfaf6e 100644 --- a/pkg/service/audit_events.go +++ b/pkg/service/audit_events.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "io" "git.itzana.me/strafesnet/go-grpc/users" "git.itzana.me/strafesnet/maps-service/pkg/api" @@ -43,18 +44,16 @@ func (svc *Service) CreateMapfixAuditComment(ctx context.Context, req api.Create } } - data := []byte{} - _, err = req.Read(data) + data, err := io.ReadAll(req) if err != nil { return err } - Comment := string(data) event_data := model.AuditEventDataComment{ - Comment: Comment, + Comment: string(data), } - EventData, err := json.Marshal(event_data) + EventData, err := json.Marshal(&event_data) if err != nil { return err } @@ -173,18 +172,16 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr } } - data := []byte{} - _, err = req.Read(data) + data, err := io.ReadAll(req) if err != nil { return err } - Comment := string(data) event_data := model.AuditEventDataComment{ - Comment: Comment, + Comment: string(data), } - EventData, err := json.Marshal(event_data) + EventData, err := json.Marshal(&event_data) if err != nil { return err } -- 2.49.1